diff --git a/crates/api/src/community/add_mod.rs b/crates/api/src/community/add_mod.rs index 5245f592e..4b65a4b10 100644 --- a/crates/api/src/community/add_mod.rs +++ b/crates/api/src/community/add_mod.rs @@ -4,7 +4,7 @@ use lemmy_api_common::{ community::{AddModToCommunity, AddModToCommunityResponse}, context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, - utils::check_community_mod_action, + utils::{check_community_mod_action, check_is_higher_mod}, }; use lemmy_db_schema::{ source::{ @@ -33,6 +33,18 @@ pub async fn add_mod_to_community( &mut context.pool(), ) .await?; + + // If its a mod removal, also check that you're a higher mod. + if !data.added { + check_is_higher_mod( + &mut context.pool(), + &local_user_view, + community_id, + &[data.person_id], + ) + .await?; + } + let community = Community::read(&mut context.pool(), community_id) .await? .ok_or(LemmyErrorType::CouldntFindCommunity)?; diff --git a/crates/api/src/local_user/add_admin.rs b/crates/api/src/local_user/add_admin.rs index 7db2e5653..0345b3c64 100644 --- a/crates/api/src/local_user/add_admin.rs +++ b/crates/api/src/local_user/add_admin.rs @@ -2,7 +2,7 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, person::{AddAdmin, AddAdminResponse}, - utils::is_admin, + utils::{check_is_higher_admin, is_admin}, }; use lemmy_db_schema::{ source::{ @@ -24,6 +24,11 @@ pub async fn add_admin( // Make sure user is an admin is_admin(&local_user_view)?; + // If its an admin removal, also check that you're a higher admin + if !data.added { + check_is_higher_admin(&mut context.pool(), &local_user_view, &[data.person_id]).await?; + } + // Make sure that the person_id added is local let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id) .await? diff --git a/crates/api/src/site/purge/comment.rs b/crates/api/src/site/purge/comment.rs index 70d95e160..66c82bb18 100644 --- a/crates/api/src/site/purge/comment.rs +++ b/crates/api/src/site/purge/comment.rs @@ -4,7 +4,7 @@ use lemmy_api_common::{ context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, site::PurgeComment, - utils::is_admin, + utils::{check_is_higher_admin, is_admin}, SuccessResponse, }; use lemmy_db_schema::{ @@ -33,6 +33,14 @@ pub async fn purge_comment( .await? .ok_or(LemmyErrorType::CouldntFindComment)?; + // Also check that you're a higher admin + check_is_higher_admin( + &mut context.pool(), + &local_user_view, + &[comment_view.creator.id], + ) + .await?; + let post_id = comment_view.comment.post_id; // TODO read comments for pictrs images and purge them diff --git a/crates/api/src/site/purge/community.rs b/crates/api/src/site/purge/community.rs index 14b250681..3768c5936 100644 --- a/crates/api/src/site/purge/community.rs +++ b/crates/api/src/site/purge/community.rs @@ -5,10 +5,11 @@ use lemmy_api_common::{ request::purge_image_from_pictrs, send_activity::{ActivityChannel, SendActivityData}, site::PurgeCommunity, - utils::{is_admin, purge_image_posts_for_community}, + utils::{check_is_higher_admin, is_admin, purge_image_posts_for_community}, SuccessResponse, }; use lemmy_db_schema::{ + newtypes::PersonId, source::{ community::Community, moderator::{AdminPurgeCommunity, AdminPurgeCommunityForm}, @@ -16,6 +17,7 @@ use lemmy_db_schema::{ traits::Crud, }; use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::CommunityModeratorView; use lemmy_utils::{error::LemmyResult, LemmyErrorType}; #[tracing::instrument(skip(context))] @@ -32,6 +34,21 @@ pub async fn purge_community( .await? .ok_or(LemmyErrorType::CouldntFindCommunity)?; + // Also check that you're a higher admin than all the mods + let community_mod_person_ids = + CommunityModeratorView::for_community(&mut context.pool(), community.id) + .await? + .iter() + .map(|cmv| cmv.moderator.id) + .collect::>(); + + check_is_higher_admin( + &mut context.pool(), + &local_user_view, + &community_mod_person_ids, + ) + .await?; + if let Some(banner) = &community.banner { purge_image_from_pictrs(banner, &context).await.ok(); } diff --git a/crates/api/src/site/purge/person.rs b/crates/api/src/site/purge/person.rs index 1b38752c7..f1e4b8637 100644 --- a/crates/api/src/site/purge/person.rs +++ b/crates/api/src/site/purge/person.rs @@ -5,7 +5,7 @@ use lemmy_api_common::{ context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, site::PurgePerson, - utils::{is_admin, purge_user_account}, + utils::{check_is_higher_admin, is_admin, purge_user_account}, SuccessResponse, }; use lemmy_db_schema::{ @@ -27,9 +27,13 @@ pub async fn purge_person( // Only let admin purge an item is_admin(&local_user_view)?; + // Also check that you're a higher admin + check_is_higher_admin(&mut context.pool(), &local_user_view, &[data.person_id]).await?; + let person = Person::read(&mut context.pool(), data.person_id) .await? .ok_or(LemmyErrorType::CouldntFindPerson)?; + ban_nonlocal_user_from_local_communities( &local_user_view, &person, diff --git a/crates/api/src/site/purge/post.rs b/crates/api/src/site/purge/post.rs index 75cd021d1..576ba295d 100644 --- a/crates/api/src/site/purge/post.rs +++ b/crates/api/src/site/purge/post.rs @@ -5,7 +5,7 @@ use lemmy_api_common::{ request::purge_image_from_pictrs, send_activity::{ActivityChannel, SendActivityData}, site::PurgePost, - utils::is_admin, + utils::{check_is_higher_admin, is_admin}, SuccessResponse, }; use lemmy_db_schema::{ @@ -32,6 +32,9 @@ pub async fn purge_post( .await? .ok_or(LemmyErrorType::CouldntFindPost)?; + // Also check that you're a higher admin + check_is_higher_admin(&mut context.pool(), &local_user_view, &[post.creator_id]).await?; + // Purge image if let Some(url) = &post.url { purge_image_from_pictrs(url, &context).await.ok(); diff --git a/crates/api_common/src/claims.rs b/crates/api_common/src/claims.rs index 6c17d4e6a..905394785 100644 --- a/crates/api_common/src/claims.rs +++ b/crates/api_common/src/claims.rs @@ -116,10 +116,7 @@ mod tests { let inserted_person = Person::create(pool, &new_person).await.unwrap(); - let local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_person.id) - .password_encrypted("123456".to_string()) - .build(); + let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]) .await diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index 97b12cc5b..e61730af6 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -23,6 +23,7 @@ use lemmy_db_schema::{ local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_site_url_blocklist::LocalSiteUrlBlocklist, + local_user::LocalUser, password_reset_request::PasswordResetRequest, person::{Person, PersonUpdateForm}, person_block::PersonBlock, @@ -144,6 +145,39 @@ pub fn is_top_mod( } } +/// Checks to make sure the acting moderator is higher than the target moderator. +pub async fn check_is_higher_mod( + pool: &mut DbPool<'_>, + local_user_view: &LocalUserView, + community_id: CommunityId, + target_person_ids: &[PersonId], +) -> LemmyResult<()> { + CommunityModerator::is_higher_mod_check( + pool, + community_id, + local_user_view.person.id, + target_person_ids, + ) + .await + .with_lemmy_type(LemmyErrorType::NotHigherMod)?; + + Ok(()) +} + +/// Checks to make sure the acting admin is higher than the target admin. +/// This needs to be done on admin removals, and all purge functions +pub async fn check_is_higher_admin( + pool: &mut DbPool<'_>, + local_user_view: &LocalUserView, + target_person_ids: &[PersonId], +) -> LemmyResult<()> { + LocalUser::is_higher_admin_check(pool, local_user_view.person.id, target_person_ids) + .await + .with_lemmy_type(LemmyErrorType::NotHigherAdmin)?; + + Ok(()) +} + /// Marks a post as read for a given person. #[tracing::instrument(skip_all)] pub async fn mark_post_as_read( diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index c84bd0a50..64bef8760 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -150,18 +150,18 @@ pub async fn register( .unwrap_or(site_view.site.content_warning.is_some()); // Create the local user - let local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_person.id) - .email(data.email.as_deref().map(str::to_lowercase)) - .password_encrypted(data.password.to_string()) - .show_nsfw(Some(show_nsfw)) - .accepted_application(accepted_application) - .default_listing_type(Some(local_site.default_post_listing_type)) - .post_listing_mode(Some(local_site.default_post_listing_mode)) - .interface_language(language_tags.first().cloned()) + let local_user_form = LocalUserInsertForm { + email: data.email.as_deref().map(str::to_lowercase), + password_encrypted: data.password.to_string(), + show_nsfw: Some(show_nsfw), + accepted_application, + default_listing_type: Some(local_site.default_post_listing_type), + post_listing_mode: Some(local_site.default_post_listing_mode), + interface_language: language_tags.first().cloned(), // If its the initial site setup, they are an admin - .admin(Some(!local_site.site_setup)) - .build(); + admin: Some(!local_site.site_setup), + ..LocalUserInsertForm::new(inserted_person.id, data.password.to_string()) + }; let all_languages = Language::read_all(&mut context.pool()).await?; // use hashset to avoid duplicates diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index 9f2cb58c5..a0879b3c9 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -345,10 +345,7 @@ mod tests { }; let person = Person::create(&mut context.pool(), &person_form).await?; - let user_form = LocalUserInsertForm::builder() - .person_id(person.id) - .password_encrypted("pass".to_string()) - .build(); + let user_form = LocalUserInsertForm::test_form(person.id); let local_user = LocalUser::create(&mut context.pool(), &user_form, vec![]).await?; Ok( diff --git a/crates/db_schema/src/impls/actor_language.rs b/crates/db_schema/src/impls/actor_language.rs index 8483d6c20..5a8658baf 100644 --- a/crates/db_schema/src/impls/actor_language.rs +++ b/crates/db_schema/src/impls/actor_language.rs @@ -533,10 +533,7 @@ mod tests { let person_form = PersonInsertForm::test_form(instance.id, "my test person"); let person = Person::create(pool, &person_form).await.unwrap(); - let local_user_form = LocalUserInsertForm::builder() - .person_id(person.id) - .password_encrypted("my_pw".to_string()) - .build(); + let local_user_form = LocalUserInsertForm::test_form(person.id); let local_user = LocalUser::create(pool, &local_user_form, vec![]) .await @@ -645,10 +642,7 @@ mod tests { let person_form = PersonInsertForm::test_form(instance.id, "my test person"); let person = Person::create(pool, &person_form).await.unwrap(); - let local_user_form = LocalUserInsertForm::builder() - .person_id(person.id) - .password_encrypted("my_pw".to_string()) - .build(); + let local_user_form = LocalUserInsertForm::test_form(person.id); let local_user = LocalUser::create(pool, &local_user_form, vec![]) .await .unwrap(); diff --git a/crates/db_schema/src/impls/community.rs b/crates/db_schema/src/impls/community.rs index 6cd90cc66..a4720e275 100644 --- a/crates/db_schema/src/impls/community.rs +++ b/crates/db_schema/src/impls/community.rs @@ -1,7 +1,14 @@ use crate::{ diesel::{DecoratableTarget, OptionalExtension}, newtypes::{CommunityId, DbUrl, PersonId}, - schema::{community, community_follower, instance}, + schema::{ + community, + community_follower, + community_moderator, + community_person_ban, + instance, + post, + }, source::{ actor_language::CommunityLanguage, community::{ @@ -83,9 +90,8 @@ impl Joinable for CommunityModerator { pool: &mut DbPool<'_>, community_moderator_form: &CommunityModeratorForm, ) -> Result { - use crate::schema::community_moderator::dsl::community_moderator; let conn = &mut get_conn(pool).await?; - insert_into(community_moderator) + insert_into(community_moderator::table) .values(community_moderator_form) .get_result::(conn) .await @@ -95,9 +101,8 @@ impl Joinable for CommunityModerator { pool: &mut DbPool<'_>, community_moderator_form: &CommunityModeratorForm, ) -> Result { - use crate::schema::community_moderator::dsl::community_moderator; let conn = &mut get_conn(pool).await?; - diesel::delete(community_moderator.find(( + diesel::delete(community_moderator::table.find(( community_moderator_form.person_id, community_moderator_form.community_id, ))) @@ -147,25 +152,23 @@ impl Community { pool: &mut DbPool<'_>, url: &DbUrl, ) -> Result, Error> { - use crate::schema::community::dsl::{featured_url, moderators_url}; - use CollectionType::*; let conn = &mut get_conn(pool).await?; let res = community::table - .filter(moderators_url.eq(url)) + .filter(community::moderators_url.eq(url)) .first(conn) .await .optional()?; if let Some(c) = res { - Ok(Some((c, Moderators))) + Ok(Some((c, CollectionType::Moderators))) } else { let res = community::table - .filter(featured_url.eq(url)) + .filter(community::featured_url.eq(url)) .first(conn) .await .optional()?; if let Some(c) = res { - Ok(Some((c, Featured))) + Ok(Some((c, CollectionType::Featured))) } else { Ok(None) } @@ -177,7 +180,6 @@ impl Community { posts: Vec, pool: &mut DbPool<'_>, ) -> Result<(), Error> { - use crate::schema::post; let conn = &mut get_conn(pool).await?; for p in &posts { debug_assert!(p.community_id == community_id); @@ -185,10 +187,10 @@ impl Community { // Mark the given posts as featured and all other posts as not featured. let post_ids = posts.iter().map(|p| p.id); update(post::table) - .filter(post::dsl::community_id.eq(community_id)) + .filter(post::community_id.eq(community_id)) // This filter is just for performance - .filter(post::dsl::featured_community.or(post::dsl::id.eq_any(post_ids.clone()))) - .set(post::dsl::featured_community.eq(post::dsl::id.eq_any(post_ids))) + .filter(post::featured_community.or(post::id.eq_any(post_ids.clone()))) + .set(post::featured_community.eq(post::id.eq_any(post_ids))) .execute(conn) .await?; Ok(()) @@ -200,37 +202,68 @@ impl CommunityModerator { pool: &mut DbPool<'_>, for_community_id: CommunityId, ) -> Result { - use crate::schema::community_moderator::dsl::{community_id, community_moderator}; let conn = &mut get_conn(pool).await?; - diesel::delete(community_moderator.filter(community_id.eq(for_community_id))) - .execute(conn) - .await + diesel::delete( + community_moderator::table.filter(community_moderator::community_id.eq(for_community_id)), + ) + .execute(conn) + .await } pub async fn leave_all_communities( pool: &mut DbPool<'_>, for_person_id: PersonId, ) -> Result { - use crate::schema::community_moderator::dsl::{community_moderator, person_id}; let conn = &mut get_conn(pool).await?; - diesel::delete(community_moderator.filter(person_id.eq(for_person_id))) - .execute(conn) - .await + diesel::delete( + community_moderator::table.filter(community_moderator::person_id.eq(for_person_id)), + ) + .execute(conn) + .await } pub async fn get_person_moderated_communities( pool: &mut DbPool<'_>, for_person_id: PersonId, ) -> Result, Error> { - use crate::schema::community_moderator::dsl::{community_id, community_moderator, person_id}; let conn = &mut get_conn(pool).await?; - community_moderator - .filter(person_id.eq(for_person_id)) - .select(community_id) + community_moderator::table + .filter(community_moderator::person_id.eq(for_person_id)) + .select(community_moderator::community_id) .load::(conn) .await } + + /// Checks to make sure the acting moderator is higher than the target moderator + pub async fn is_higher_mod_check( + pool: &mut DbPool<'_>, + for_community_id: CommunityId, + mod_person_id: PersonId, + target_person_ids: &[PersonId], + ) -> Result { + let conn = &mut get_conn(pool).await?; + + // Build the list of persons + let mut persons = target_person_ids.to_owned(); + persons.push(mod_person_id); + persons.dedup(); + + let res = community_moderator::table + .filter(community_moderator::community_id.eq(for_community_id)) + .filter(community_moderator::person_id.eq_any(persons)) + .order_by(community_moderator::published) + // This does a limit 1 select first + .first::(conn) + .await?; + + // If the first result sorted by published is the acting mod + if res.person_id == mod_person_id { + Ok(true) + } else { + Err(diesel::result::Error::NotFound) + } + } } #[async_trait] @@ -240,11 +273,13 @@ impl Bannable for CommunityPersonBan { pool: &mut DbPool<'_>, community_person_ban_form: &CommunityPersonBanForm, ) -> Result { - use crate::schema::community_person_ban::dsl::{community_id, community_person_ban, person_id}; let conn = &mut get_conn(pool).await?; - insert_into(community_person_ban) + insert_into(community_person_ban::table) .values(community_person_ban_form) - .on_conflict((community_id, person_id)) + .on_conflict(( + community_person_ban::community_id, + community_person_ban::person_id, + )) .do_update() .set(community_person_ban_form) .get_result::(conn) @@ -255,9 +290,8 @@ impl Bannable for CommunityPersonBan { pool: &mut DbPool<'_>, community_person_ban_form: &CommunityPersonBanForm, ) -> Result { - use crate::schema::community_person_ban::dsl::community_person_ban; let conn = &mut get_conn(pool).await?; - diesel::delete(community_person_ban.find(( + diesel::delete(community_person_ban::table.find(( community_person_ban_form.person_id, community_person_ban_form.community_id, ))) @@ -291,11 +325,10 @@ impl CommunityFollower { pool: &mut DbPool<'_>, remote_community_id: CommunityId, ) -> Result { - use crate::schema::community_follower::dsl::{community_follower, community_id}; let conn = &mut get_conn(pool).await?; - select(exists( - community_follower.filter(community_id.eq(remote_community_id)), - )) + select(exists(community_follower::table.filter( + community_follower::community_id.eq(remote_community_id), + ))) .get_result(conn) .await } @@ -316,11 +349,13 @@ impl Queryable, Pg> for SubscribedType { impl Followable for CommunityFollower { type Form = CommunityFollowerForm; async fn follow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result { - use crate::schema::community_follower::dsl::{community_follower, community_id, person_id}; let conn = &mut get_conn(pool).await?; - insert_into(community_follower) + insert_into(community_follower::table) .values(form) - .on_conflict((community_id, person_id)) + .on_conflict(( + community_follower::community_id, + community_follower::person_id, + )) .do_update() .set(form) .get_result::(conn) @@ -331,17 +366,16 @@ impl Followable for CommunityFollower { community_id: CommunityId, person_id: PersonId, ) -> Result { - use crate::schema::community_follower::dsl::{community_follower, pending}; let conn = &mut get_conn(pool).await?; - diesel::update(community_follower.find((person_id, community_id))) - .set(pending.eq(false)) + diesel::update(community_follower::table.find((person_id, community_id))) + .set(community_follower::pending.eq(false)) .get_result::(conn) .await } + async fn unfollow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result { - use crate::schema::community_follower::dsl::community_follower; let conn = &mut get_conn(pool).await?; - diesel::delete(community_follower.find((form.person_id, form.community_id))) + diesel::delete(community_follower::table.find((form.person_id, form.community_id))) .execute(conn) .await } @@ -397,10 +431,8 @@ impl ApubActor for Community { } #[cfg(test)] -#[allow(clippy::unwrap_used)] #[allow(clippy::indexing_slicing)] mod tests { - use crate::{ source::{ community::{ @@ -421,22 +453,23 @@ mod tests { utils::build_db_pool_for_tests, CommunityVisibility, }; + use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - let new_person = PersonInsertForm::test_form(inserted_instance.id, "bobbee"); + let bobby_person = PersonInsertForm::test_form(inserted_instance.id, "bobby"); + let inserted_bobby = Person::create(pool, &bobby_person).await?; - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let artemis_person = PersonInsertForm::test_form(inserted_instance.id, "artemis"); + let inserted_artemis = Person::create(pool, &artemis_person).await?; let new_community = CommunityInsertForm::builder() .name("TIL".into()) @@ -445,7 +478,7 @@ mod tests { .instance_id(inserted_instance.id) .build(); - let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let inserted_community = Community::create(pool, &new_community).await?; let expected_community = Community { id: inserted_community.id, @@ -477,91 +510,110 @@ mod tests { let community_follower_form = CommunityFollowerForm { community_id: inserted_community.id, - person_id: inserted_person.id, + person_id: inserted_bobby.id, pending: false, }; - let inserted_community_follower = CommunityFollower::follow(pool, &community_follower_form) - .await - .unwrap(); + let inserted_community_follower = + CommunityFollower::follow(pool, &community_follower_form).await?; let expected_community_follower = CommunityFollower { community_id: inserted_community.id, - person_id: inserted_person.id, + person_id: inserted_bobby.id, pending: false, published: inserted_community_follower.published, }; - let community_moderator_form = CommunityModeratorForm { + let bobby_moderator_form = CommunityModeratorForm { community_id: inserted_community.id, - person_id: inserted_person.id, + person_id: inserted_bobby.id, }; - let inserted_community_moderator = CommunityModerator::join(pool, &community_moderator_form) - .await - .unwrap(); + let inserted_bobby_moderator = CommunityModerator::join(pool, &bobby_moderator_form).await?; + + let artemis_moderator_form = CommunityModeratorForm { + community_id: inserted_community.id, + person_id: inserted_artemis.id, + }; + + let _inserted_artemis_moderator = + CommunityModerator::join(pool, &artemis_moderator_form).await?; let expected_community_moderator = CommunityModerator { community_id: inserted_community.id, - person_id: inserted_person.id, - published: inserted_community_moderator.published, + person_id: inserted_bobby.id, + published: inserted_bobby_moderator.published, }; + let moderator_person_ids = vec![inserted_bobby.id, inserted_artemis.id]; + + // Make sure bobby is marked as a higher mod than artemis, and vice versa + let bobby_higher_check = CommunityModerator::is_higher_mod_check( + pool, + inserted_community.id, + inserted_bobby.id, + &moderator_person_ids, + ) + .await?; + assert!(bobby_higher_check); + + // This should throw an error, since artemis was added later + let artemis_higher_check = CommunityModerator::is_higher_mod_check( + pool, + inserted_community.id, + inserted_artemis.id, + &moderator_person_ids, + ) + .await; + assert!(artemis_higher_check.is_err()); + let community_person_ban_form = CommunityPersonBanForm { community_id: inserted_community.id, - person_id: inserted_person.id, + person_id: inserted_bobby.id, expires: None, }; - let inserted_community_person_ban = CommunityPersonBan::ban(pool, &community_person_ban_form) - .await - .unwrap(); + let inserted_community_person_ban = + CommunityPersonBan::ban(pool, &community_person_ban_form).await?; let expected_community_person_ban = CommunityPersonBan { community_id: inserted_community.id, - person_id: inserted_person.id, + person_id: inserted_bobby.id, published: inserted_community_person_ban.published, expires: None, }; let read_community = Community::read(pool, inserted_community.id) - .await - .unwrap() - .unwrap(); + .await? + .ok_or(LemmyErrorType::CouldntFindCommunity)?; let update_community_form = CommunityUpdateForm { title: Some("nada".to_owned()), ..Default::default() }; - let updated_community = Community::update(pool, inserted_community.id, &update_community_form) - .await - .unwrap(); + let updated_community = + Community::update(pool, inserted_community.id, &update_community_form).await?; - let ignored_community = CommunityFollower::unfollow(pool, &community_follower_form) - .await - .unwrap(); - let left_community = CommunityModerator::leave(pool, &community_moderator_form) - .await - .unwrap(); - let unban = CommunityPersonBan::unban(pool, &community_person_ban_form) - .await - .unwrap(); - let num_deleted = Community::delete(pool, inserted_community.id) - .await - .unwrap(); - Person::delete(pool, inserted_person.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + let ignored_community = CommunityFollower::unfollow(pool, &community_follower_form).await?; + let left_community = CommunityModerator::leave(pool, &bobby_moderator_form).await?; + let unban = CommunityPersonBan::unban(pool, &community_person_ban_form).await?; + let num_deleted = Community::delete(pool, inserted_community.id).await?; + Person::delete(pool, inserted_bobby.id).await?; + Person::delete(pool, inserted_artemis.id).await?; + Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_community, read_community); assert_eq!(expected_community, inserted_community); assert_eq!(expected_community, updated_community); assert_eq!(expected_community_follower, inserted_community_follower); - assert_eq!(expected_community_moderator, inserted_community_moderator); + assert_eq!(expected_community_moderator, inserted_bobby_moderator); assert_eq!(expected_community_person_ban, inserted_community_person_ban); assert_eq!(1, ignored_community); assert_eq!(1, left_community); assert_eq!(1, unban); // assert_eq!(2, loaded_count); assert_eq!(1, num_deleted); + + Ok(()) } } diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index 9b59e07ba..a7702e6eb 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -215,6 +215,35 @@ impl LocalUser { blocked_instances, }) } + + /// Checks to make sure the acting moderator is higher than the target moderator + pub async fn is_higher_admin_check( + pool: &mut DbPool<'_>, + admin_person_id: PersonId, + target_person_ids: &[PersonId], + ) -> Result { + let conn = &mut get_conn(pool).await?; + + // Build the list of persons + let mut persons = target_person_ids.to_owned(); + persons.push(admin_person_id); + persons.dedup(); + + let res = local_user::table + .filter(local_user::admin.eq(true)) + .filter(local_user::person_id.eq_any(persons)) + .order_by(local_user::id) + // This does a limit 1 select first + .first::(conn) + .await?; + + // If the first result sorted by published is the acting mod + if res.person_id == admin_person_id { + Ok(true) + } else { + Err(diesel::result::Error::NotFound) + } + } } /// Adds some helper functions for an optional LocalUser @@ -257,10 +286,14 @@ impl LocalUserOptionHelper for Option<&LocalUser> { impl LocalUserInsertForm { pub fn test_form(person_id: PersonId) -> Self { - Self::builder() - .person_id(person_id) - .password_encrypted(String::new()) - .build() + Self::new(person_id, String::new()) + } + + pub fn test_form_admin(person_id: PersonId) -> Self { + LocalUserInsertForm { + admin: Some(true), + ..Self::test_form(person_id) + } } } @@ -272,3 +305,57 @@ pub struct UserBackupLists { pub blocked_users: Vec, pub blocked_instances: Vec, } + +#[cfg(test)] +#[allow(clippy::indexing_slicing)] +mod tests { + use crate::{ + source::{ + instance::Instance, + local_user::{LocalUser, LocalUserInsertForm}, + person::{Person, PersonInsertForm}, + }, + traits::Crud, + utils::build_db_pool_for_tests, + }; + use lemmy_utils::error::LemmyResult; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn test_admin_higher_check() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests().await; + let pool = &mut pool.into(); + + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let fiona_person = PersonInsertForm::test_form(inserted_instance.id, "fiona"); + let inserted_fiona_person = Person::create(pool, &fiona_person).await?; + + let fiona_local_user_form = LocalUserInsertForm::test_form_admin(inserted_fiona_person.id); + let _inserted_fiona_local_user = + LocalUser::create(pool, &fiona_local_user_form, vec![]).await?; + + let delores_person = PersonInsertForm::test_form(inserted_instance.id, "delores"); + let inserted_delores_person = Person::create(pool, &delores_person).await?; + let delores_local_user_form = LocalUserInsertForm::test_form_admin(inserted_delores_person.id); + let _inserted_delores_local_user = + LocalUser::create(pool, &delores_local_user_form, vec![]).await?; + + let admin_person_ids = vec![inserted_fiona_person.id, inserted_delores_person.id]; + + // Make sure fiona is marked as a higher admin than delores, and vice versa + let fiona_higher_check = + LocalUser::is_higher_admin_check(pool, inserted_fiona_person.id, &admin_person_ids).await?; + assert!(fiona_higher_check); + + // This should throw an error, since delores was added later + let delores_higher_check = + LocalUser::is_higher_admin_check(pool, inserted_delores_person.id, &admin_person_ids).await; + assert!(delores_higher_check.is_err()); + + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) + } +} diff --git a/crates/db_schema/src/impls/password_reset_request.rs b/crates/db_schema/src/impls/password_reset_request.rs index 0b1351af1..be05ed8ac 100644 --- a/crates/db_schema/src/impls/password_reset_request.rs +++ b/crates/db_schema/src/impls/password_reset_request.rs @@ -72,10 +72,7 @@ mod tests { let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy prw"); let inserted_person = Person::create(pool, &new_person).await?; - let new_local_user = LocalUserInsertForm::builder() - .person_id(inserted_person.id) - .password_encrypted("pass".to_string()) - .build(); + let new_local_user = LocalUserInsertForm::test_form(inserted_person.id); let inserted_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?; // Create password reset token diff --git a/crates/db_schema/src/impls/person.rs b/crates/db_schema/src/impls/person.rs index f318a503a..89c108f8c 100644 --- a/crates/db_schema/src/impls/person.rs +++ b/crates/db_schema/src/impls/person.rs @@ -182,11 +182,10 @@ impl ApubActor for Person { impl Followable for PersonFollower { type Form = PersonFollowerForm; async fn follow(pool: &mut DbPool<'_>, form: &PersonFollowerForm) -> Result { - use crate::schema::person_follower::dsl::{follower_id, person_follower, person_id}; let conn = &mut get_conn(pool).await?; - insert_into(person_follower) + insert_into(person_follower::table) .values(form) - .on_conflict((follower_id, person_id)) + .on_conflict((person_follower::follower_id, person_follower::person_id)) .do_update() .set(form) .get_result::(conn) @@ -196,9 +195,8 @@ impl Followable for PersonFollower { unimplemented!() } async fn unfollow(pool: &mut DbPool<'_>, form: &PersonFollowerForm) -> Result { - use crate::schema::person_follower::dsl::person_follower; let conn = &mut get_conn(pool).await?; - diesel::delete(person_follower.find((form.follower_id, form.person_id))) + diesel::delete(person_follower::table.find((form.follower_id, form.person_id))) .execute(conn) .await } @@ -220,7 +218,6 @@ impl PersonFollower { } #[cfg(test)] -#[allow(clippy::unwrap_used)] #[allow(clippy::indexing_slicing)] mod tests { @@ -232,22 +229,21 @@ mod tests { traits::{Crud, Followable}, utils::build_db_pool_for_tests, }; + use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use pretty_assertions::assert_eq; use serial_test::serial; #[tokio::test] #[serial] - async fn test_crud() { + async fn test_crud() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "holly"); - let inserted_person = Person::create(pool, &new_person).await.unwrap(); + let inserted_person = Person::create(pool, &new_person).await?; let expected_person = Person { id: inserted_person.id, @@ -274,57 +270,54 @@ mod tests { }; let read_person = Person::read(pool, inserted_person.id) - .await - .unwrap() - .unwrap(); + .await? + .ok_or(LemmyErrorType::CouldntFindPerson)?; let update_person_form = PersonUpdateForm { actor_id: Some(inserted_person.actor_id.clone()), ..Default::default() }; - let updated_person = Person::update(pool, inserted_person.id, &update_person_form) - .await - .unwrap(); + let updated_person = Person::update(pool, inserted_person.id, &update_person_form).await?; - let num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); - Instance::delete(pool, inserted_instance.id).await.unwrap(); + let num_deleted = Person::delete(pool, inserted_person.id).await?; + Instance::delete(pool, inserted_instance.id).await?; assert_eq!(expected_person, read_person); assert_eq!(expected_person, inserted_person); assert_eq!(expected_person, updated_person); assert_eq!(1, num_deleted); + + Ok(()) } #[tokio::test] #[serial] - async fn follow() { + async fn follow() -> LemmyResult<()> { let pool = &build_db_pool_for_tests().await; let pool = &mut pool.into(); - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) - .await - .unwrap(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let person_form_1 = PersonInsertForm::test_form(inserted_instance.id, "erich"); - let person_1 = Person::create(pool, &person_form_1).await.unwrap(); + let person_1 = Person::create(pool, &person_form_1).await?; let person_form_2 = PersonInsertForm::test_form(inserted_instance.id, "michele"); - let person_2 = Person::create(pool, &person_form_2).await.unwrap(); + let person_2 = Person::create(pool, &person_form_2).await?; let follow_form = PersonFollowerForm { person_id: person_1.id, follower_id: person_2.id, pending: false, }; - let person_follower = PersonFollower::follow(pool, &follow_form).await.unwrap(); + let person_follower = PersonFollower::follow(pool, &follow_form).await?; assert_eq!(person_1.id, person_follower.person_id); assert_eq!(person_2.id, person_follower.follower_id); assert!(!person_follower.pending); - let followers = PersonFollower::list_followers(pool, person_1.id) - .await - .unwrap(); + let followers = PersonFollower::list_followers(pool, person_1.id).await?; assert_eq!(vec![person_2], followers); - let unfollow = PersonFollower::unfollow(pool, &follow_form).await.unwrap(); + let unfollow = PersonFollower::unfollow(pool, &follow_form).await?; assert_eq!(1, unfollow); + + Ok(()) } } diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index 5d592ddf1..c7a5b5224 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -11,7 +11,6 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; -use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -69,38 +68,59 @@ pub struct LocalUser { pub collapse_bot_comments: bool, } -#[derive(Clone, TypedBuilder)] -#[builder(field_defaults(default))] +#[derive(Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable))] #[cfg_attr(feature = "full", diesel(table_name = local_user))] pub struct LocalUserInsertForm { - #[builder(!default)] pub person_id: PersonId, - #[builder(!default)] pub password_encrypted: String, + #[new(default)] pub email: Option, + #[new(default)] pub show_nsfw: Option, + #[new(default)] pub theme: Option, + #[new(default)] pub default_sort_type: Option, + #[new(default)] pub default_listing_type: Option, + #[new(default)] pub interface_language: Option, + #[new(default)] pub show_avatars: Option, + #[new(default)] pub send_notifications_to_email: Option, + #[new(default)] pub show_bot_accounts: Option, + #[new(default)] pub show_scores: Option, + #[new(default)] pub show_read_posts: Option, + #[new(default)] pub email_verified: Option, + #[new(default)] pub accepted_application: Option, + #[new(default)] pub totp_2fa_secret: Option>, + #[new(default)] pub open_links_in_new_tab: Option, + #[new(default)] pub blur_nsfw: Option, + #[new(default)] pub auto_expand: Option, + #[new(default)] pub infinite_scroll_enabled: Option, + #[new(default)] pub admin: Option, + #[new(default)] pub post_listing_mode: Option, + #[new(default)] pub totp_2fa_enabled: Option, + #[new(default)] pub enable_keyboard_navigation: Option, + #[new(default)] pub enable_animated_images: Option, + #[new(default)] pub collapse_bot_comments: Option, } diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index 950d061ba..d7b26a1ed 100644 --- a/crates/db_views/src/comment_report_view.rs +++ b/crates/db_views/src/comment_report_view.rs @@ -301,10 +301,7 @@ mod tests { let inserted_timmy = Person::create(pool, &new_person).await.unwrap(); - let new_local_user = LocalUserInsertForm::builder() - .person_id(inserted_timmy.id) - .password_encrypted("123".to_string()) - .build(); + let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id); let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]) .await .unwrap(); diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index 61dbceb4b..9b99b842e 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -490,11 +490,8 @@ mod tests { let timmy_person_form = PersonInsertForm::test_form(inserted_instance.id, "timmy"); let inserted_timmy_person = Person::create(pool, &timmy_person_form).await?; - let timmy_local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_timmy_person.id) - .admin(Some(true)) - .password_encrypted(String::new()) - .build(); + let timmy_local_user_form = LocalUserInsertForm::test_form_admin(inserted_timmy_person.id); + let inserted_timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; let sara_person_form = PersonInsertForm::test_form(inserted_instance.id, "sara"); diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index e89b7d545..0cd06dd4e 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -323,10 +323,7 @@ mod tests { let inserted_timmy = Person::create(pool, &new_person).await.unwrap(); - let new_local_user = LocalUserInsertForm::builder() - .person_id(inserted_timmy.id) - .password_encrypted("123".to_string()) - .build(); + let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id); let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]) .await .unwrap(); diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index cd63859af..9a85d534a 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -167,11 +167,7 @@ mod tests { let inserted_timmy_person = Person::create(pool, &timmy_person_form).await.unwrap(); - let timmy_local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_timmy_person.id) - .password_encrypted("nada".to_string()) - .admin(Some(true)) - .build(); + let timmy_local_user_form = LocalUserInsertForm::test_form_admin(inserted_timmy_person.id); let _inserted_timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]) .await @@ -181,10 +177,7 @@ mod tests { let inserted_sara_person = Person::create(pool, &sara_person_form).await.unwrap(); - let sara_local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_sara_person.id) - .password_encrypted("nada".to_string()) - .build(); + let sara_local_user_form = LocalUserInsertForm::test_form(inserted_sara_person.id); let inserted_sara_local_user = LocalUser::create(pool, &sara_local_user_form, vec![]) .await @@ -209,10 +202,7 @@ mod tests { let inserted_jess_person = Person::create(pool, &jess_person_form).await.unwrap(); - let jess_local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_jess_person.id) - .password_encrypted("nada".to_string()) - .build(); + let jess_local_user_form = LocalUserInsertForm::test_form(inserted_jess_person.id); let inserted_jess_local_user = LocalUser::create(pool, &jess_local_user_form, vec![]) .await diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index 25e76c7b3..0181cfd53 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -288,10 +288,7 @@ mod tests { let inserted_person = Person::create(pool, &new_person).await.unwrap(); - let local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_person.id) - .password_encrypted(String::new()) - .build(); + let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); let local_user = LocalUser::create(pool, &local_user_form, vec![]) .await .unwrap(); diff --git a/crates/db_views_actor/src/person_view.rs b/crates/db_views_actor/src/person_view.rs index 98a0ca38d..26e4ee680 100644 --- a/crates/db_views_actor/src/person_view.rs +++ b/crates/db_views_actor/src/person_view.rs @@ -196,10 +196,7 @@ mod tests { ..PersonInsertForm::test_form(inserted_instance.id, "alice") }; let alice = Person::create(pool, &alice_form).await?; - let alice_local_user_form = LocalUserInsertForm::builder() - .person_id(alice.id) - .password_encrypted(String::new()) - .build(); + let alice_local_user_form = LocalUserInsertForm::test_form(alice.id); let alice_local_user = LocalUser::create(pool, &alice_local_user_form, vec![]).await?; let bob_form = PersonInsertForm { @@ -208,10 +205,7 @@ mod tests { ..PersonInsertForm::test_form(inserted_instance.id, "bob") }; let bob = Person::create(pool, &bob_form).await?; - let bob_local_user_form = LocalUserInsertForm::builder() - .person_id(bob.id) - .password_encrypted(String::new()) - .build(); + let bob_local_user_form = LocalUserInsertForm::test_form(bob.id); let bob_local_user = LocalUser::create(pool, &bob_local_user_form, vec![]).await?; Ok(Data { diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index 324c08ccb..b848916b2 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -38,6 +38,8 @@ pub enum LemmyErrorType { NotTopAdmin, NotTopMod, NotLoggedIn, + NotHigherMod, + NotHigherAdmin, SiteBan, Deleted, BannedFromCommunity, diff --git a/src/code_migrations.rs b/src/code_migrations.rs index fd4ef66de..ae6155bb6 100644 --- a/src/code_migrations.rs +++ b/src/code_migrations.rs @@ -468,12 +468,11 @@ async fn initialize_local_site_2022_10_10( }; let person_inserted = Person::create(pool, &person_form).await?; - let local_user_form = LocalUserInsertForm::builder() - .person_id(person_inserted.id) - .password_encrypted(setup.admin_password.clone()) - .email(setup.admin_email.clone()) - .admin(Some(true)) - .build(); + let local_user_form = LocalUserInsertForm { + email: setup.admin_email.clone(), + admin: Some(true), + ..LocalUserInsertForm::new(person_inserted.id, setup.admin_password.clone()) + }; LocalUser::create(pool, &local_user_form, vec![]).await?; }; diff --git a/src/session_middleware.rs b/src/session_middleware.rs index 8b3090a47..a72d84920 100644 --- a/src/session_middleware.rs +++ b/src/session_middleware.rs @@ -146,10 +146,7 @@ mod tests { let inserted_person = Person::create(pool, &new_person).await.unwrap(); - let local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_person.id) - .password_encrypted("123456".to_string()) - .build(); + let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]) .await