Federated Moderation #185

Manually merged
dessalines merged 20 commits from federated-moderation into main 2021-03-24 15:49:38 +00:00
6 changed files with 117 additions and 70 deletions
Showing only changes of commit a2698dea92 - Show all commits

View File

@ -1,4 +1,7 @@
use crate::{activities::receive::verify_activity_domains_valid, inbox::is_addressed_to_public}; use crate::{
activities::receive::verify_activity_domains_valid,
inbox::verify_is_addressed_to_public,
};
use activitystreams::{ use activitystreams::{
activity::{ActorAndObjectRefExt, Delete, Remove, Undo}, activity::{ActorAndObjectRefExt, Delete, Remove, Undo},
base::{AnyBase, ExtendsExt}, base::{AnyBase, ExtendsExt},
@ -47,7 +50,7 @@ pub(crate) async fn receive_remove_community(
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let remove = Remove::from_any_base(activity)?.context(location_info!())?; let remove = Remove::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&remove, expected_domain, true)?; verify_activity_domains_valid(&remove, expected_domain, true)?;
is_addressed_to_public(&remove)?; verify_is_addressed_to_public(&remove)?;
let community_uri = remove let community_uri = remove
.object() .object()
@ -89,11 +92,11 @@ pub(crate) async fn receive_undo_delete_community(
community: Community, community: Community,
expected_domain: &Url, expected_domain: &Url,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
is_addressed_to_public(&undo)?; verify_is_addressed_to_public(&undo)?;
let inner = undo.object().to_owned().one().context(location_info!())?; let inner = undo.object().to_owned().one().context(location_info!())?;
let delete = Delete::from_any_base(inner)?.context(location_info!())?; let delete = Delete::from_any_base(inner)?.context(location_info!())?;
verify_activity_domains_valid(&delete, expected_domain, true)?; verify_activity_domains_valid(&delete, expected_domain, true)?;
is_addressed_to_public(&delete)?; verify_is_addressed_to_public(&delete)?;
let deleted_community = blocking(context.pool(), move |conn| { let deleted_community = blocking(context.pool(), move |conn| {
Community::update_deleted(conn, community.id, false) Community::update_deleted(conn, community.id, false)
@ -124,12 +127,12 @@ pub(crate) async fn receive_undo_remove_community(
undo: Undo, undo: Undo,
expected_domain: &Url, expected_domain: &Url,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
is_addressed_to_public(&undo)?; verify_is_addressed_to_public(&undo)?;
let inner = undo.object().to_owned().one().context(location_info!())?; let inner = undo.object().to_owned().one().context(location_info!())?;
let remove = Remove::from_any_base(inner)?.context(location_info!())?; let remove = Remove::from_any_base(inner)?.context(location_info!())?;
verify_activity_domains_valid(&remove, &expected_domain, true)?; verify_activity_domains_valid(&remove, &expected_domain, true)?;
is_addressed_to_public(&remove)?; verify_is_addressed_to_public(&remove)?;
let community_uri = remove let community_uri = remove
.object() .object()

View File

@ -219,7 +219,8 @@ pub async fn send_add_mod(
add add
.set_many_contexts(lemmy_context()?) .set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(AddType::Add)?) .set_id(generate_activity_id(AddType::Add)?)
.set_many_tos(vec![community.actor_id.to_owned().into_inner(), public()]) .set_to(public())
.set_many_ccs(vec![community.actor_id()])
.set_target(generate_moderators_url(&community.actor_id)?.into_inner()); .set_target(generate_moderators_url(&community.actor_id)?.into_inner());
if community.local { if community.local {
@ -245,7 +246,8 @@ pub async fn send_remove_mod(
remove remove
.set_many_contexts(lemmy_context()?) .set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(RemoveType::Remove)?) .set_id(generate_activity_id(RemoveType::Remove)?)
.set_many_tos(vec![community.actor_id.to_owned().into_inner(), public()]) .set_to(public())
.set_many_ccs(vec![community.actor_id()])
.set_target(generate_moderators_url(&community.actor_id)?.into_inner()); .set_target(generate_moderators_url(&community.actor_id)?.into_inner());
if community.local { if community.local {

View File

@ -6,7 +6,6 @@ use crate::{
get_activity_to_and_cc, get_activity_to_and_cc,
inbox_verify_http_signature, inbox_verify_http_signature,
is_activity_already_known, is_activity_already_known,
is_addressed_to_public,
receive_for_community::{ receive_for_community::{
receive_add_for_community, receive_add_for_community,
receive_create_for_community, receive_create_for_community,
@ -17,6 +16,7 @@ use crate::{
receive_undo_for_community, receive_undo_for_community,
receive_update_for_community, receive_update_for_community,
}, },
verify_is_addressed_to_public,
}, },
insert_activity, insert_activity,
ActorType, ActorType,
@ -164,18 +164,18 @@ pub(crate) async fn community_receive_message(
true true
} }
CommunityValidTypes::Add => { CommunityValidTypes::Add => {
receive_add_for_community(context, any_base.clone(), &actor_url, request_counter).await?; receive_add_for_community(context, any_base.clone(), None, request_counter).await?;
true true
} }
CommunityValidTypes::Remove => { CommunityValidTypes::Remove => {
receive_remove_for_community(context, any_base.clone(), &actor_url, request_counter).await?; receive_remove_for_community(context, any_base.clone(), None, request_counter).await?;
true true
} }
}; };
if do_announce { if do_announce {
// Check again that the activity is public, just to be sure // Check again that the activity is public, just to be sure
is_addressed_to_public(&activity)?; verify_is_addressed_to_public(&activity)?;
to_community to_community
.send_announce(activity.into_any_base()?, context) .send_announce(activity.into_any_base()?, context)
.await?; .await?;

View File

@ -84,7 +84,7 @@ where
to_and_cc to_and_cc
} }
pub(crate) fn is_addressed_to_public<T, Kind>(activity: &T) -> Result<(), LemmyError> pub(crate) fn verify_is_addressed_to_public<T, Kind>(activity: &T) -> Result<(), LemmyError>
where where
T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt, T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt,
{ {

View File

@ -36,7 +36,8 @@ use crate::{
user::get_or_fetch_and_upsert_user, user::get_or_fetch_and_upsert_user,
}, },
find_post_or_comment_by_id, find_post_or_comment_by_id,
inbox::is_addressed_to_public, generate_moderators_url,
inbox::verify_is_addressed_to_public,
ActorType, ActorType,
PostOrComment, PostOrComment,
}; };
@ -44,6 +45,7 @@ use activitystreams::{
activity::{ activity::{
ActorAndObjectRef, ActorAndObjectRef,
Add, Add,
Announce,
Create, Create,
Delete, Delete,
Dislike, Dislike,
@ -54,6 +56,7 @@ use activitystreams::{
Update, Update,
}, },
base::AnyBase, base::AnyBase,
object::AsObject,
prelude::*, prelude::*,
}; };
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
@ -92,7 +95,7 @@ pub(in crate::inbox) async fn receive_create_for_community(
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let create = Create::from_any_base(activity)?.context(location_info!())?; let create = Create::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&create, &expected_domain, true)?; verify_activity_domains_valid(&create, &expected_domain, true)?;
is_addressed_to_public(&create)?; verify_is_addressed_to_public(&create)?;
let kind = create let kind = create
.object() .object()
@ -114,7 +117,7 @@ pub(in crate::inbox) async fn receive_update_for_community(
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let update = Update::from_any_base(activity)?.context(location_info!())?; let update = Update::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&update, &expected_domain, true)?; verify_activity_domains_valid(&update, &expected_domain, true)?;
is_addressed_to_public(&update)?; verify_is_addressed_to_public(&update)?;
let kind = update let kind = update
.object() .object()
@ -136,7 +139,7 @@ pub(in crate::inbox) async fn receive_like_for_community(
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let like = Like::from_any_base(activity)?.context(location_info!())?; let like = Like::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&like, &expected_domain, false)?; verify_activity_domains_valid(&like, &expected_domain, false)?;
is_addressed_to_public(&like)?; verify_is_addressed_to_public(&like)?;
let object_id = like let object_id = like
.object() .object()
@ -167,7 +170,7 @@ pub(in crate::inbox) async fn receive_dislike_for_community(
let dislike = Dislike::from_any_base(activity)?.context(location_info!())?; let dislike = Dislike::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&dislike, &expected_domain, false)?; verify_activity_domains_valid(&dislike, &expected_domain, false)?;
is_addressed_to_public(&dislike)?; verify_is_addressed_to_public(&dislike)?;
let object_id = dislike let object_id = dislike
.object() .object()
@ -191,7 +194,7 @@ pub(in crate::inbox) async fn receive_delete_for_community(
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let delete = Delete::from_any_base(activity)?.context(location_info!())?; let delete = Delete::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&delete, &expected_domain, true)?; verify_activity_domains_valid(&delete, &expected_domain, true)?;
is_addressed_to_public(&delete)?; verify_is_addressed_to_public(&delete)?;
let object = delete let object = delete
.object() .object()
@ -210,18 +213,18 @@ pub(in crate::inbox) async fn receive_delete_for_community(
/// A post or comment being removed by a mod/admin /// A post or comment being removed by a mod/admin
pub(in crate::inbox) async fn receive_remove_for_community( pub(in crate::inbox) async fn receive_remove_for_community(
context: &LemmyContext, context: &LemmyContext,
activity: AnyBase, remove_any_base: AnyBase,
expected_domain: &Url, announce: Option<Announce>,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let remove = Remove::from_any_base(activity.to_owned())?.context(location_info!())?; let remove = Remove::from_any_base(remove_any_base.to_owned())?.context(location_info!())?;
verify_activity_domains_valid(&remove, &expected_domain, false)?; let community = extract_community_from_cc(&remove, context).await?;
is_addressed_to_public(&remove)?;
verify_mod_activity(&remove, announce, &community, context).await?;
verify_is_addressed_to_public(&remove)?;
verify_actor_is_community_mod(&remove, &community, context).await?;
// Remove a moderator from community
if remove.target().is_some() { if remove.target().is_some() {
let community = verify_actor_is_community_mod(&remove, context).await?;
let remove_mod = remove let remove_mod = remove
.object() .object()
.as_single_xsd_any_uri() .as_single_xsd_any_uri()
@ -235,32 +238,18 @@ pub(in crate::inbox) async fn receive_remove_for_community(
CommunityModerator::leave(conn, &form) CommunityModerator::leave(conn, &form)
}) })
.await??; .await??;
community.send_announce(activity, context).await?; community.send_announce(remove_any_base, context).await?;
// TODO: send websocket notification about removed mod // TODO: send websocket notification about removed mod
Ok(()) Ok(())
} }
// Remove a post or comment // Remove a post or comment
else { else {
let cc = remove
.cc()
.map(|c| c.as_many())
.flatten()
.context(location_info!())?;
let community_id = cc
.first()
.map(|c| c.as_xsd_any_uri())
.flatten()
.context(location_info!())?;
let object = remove let object = remove
.object() .object()
.to_owned() .to_owned()
.single_xsd_any_uri() .single_xsd_any_uri()
.context(location_info!())?; .context(location_info!())?;
// Ensure that remove activity comes from the same domain as the community
remove.id(community_id.domain().context(location_info!())?)?;
match find_post_or_comment_by_id(context, object).await { match find_post_or_comment_by_id(context, object).await {
Ok(PostOrComment::Post(p)) => receive_remove_post(context, remove, *p).await, Ok(PostOrComment::Post(p)) => receive_remove_post(context, remove, *p).await,
Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, remove, *c).await, Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, remove, *c).await,
@ -287,7 +276,7 @@ pub(in crate::inbox) async fn receive_undo_for_community(
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let undo = Undo::from_any_base(activity)?.context(location_info!())?; let undo = Undo::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?; verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?;
is_addressed_to_public(&undo)?; verify_is_addressed_to_public(&undo)?;
use UndoableActivities::*; use UndoableActivities::*;
match undo match undo
@ -316,7 +305,7 @@ pub(in crate::inbox) async fn receive_undo_delete_for_community(
let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)? let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?; .context(location_info!())?;
verify_activity_domains_valid(&delete, &expected_domain, true)?; verify_activity_domains_valid(&delete, &expected_domain, true)?;
is_addressed_to_public(&delete)?; verify_is_addressed_to_public(&delete)?;
let object = delete let object = delete
.object() .object()
@ -340,7 +329,7 @@ pub(in crate::inbox) async fn receive_undo_remove_for_community(
let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)? let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?; .context(location_info!())?;
verify_activity_domains_valid(&remove, &expected_domain, false)?; verify_activity_domains_valid(&remove, &expected_domain, false)?;
is_addressed_to_public(&remove)?; verify_is_addressed_to_public(&remove)?;
let object = remove let object = remove
.object() .object()
@ -365,7 +354,7 @@ pub(in crate::inbox) async fn receive_undo_like_for_community(
let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)? let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?; .context(location_info!())?;
verify_activity_domains_valid(&like, &expected_domain, false)?; verify_activity_domains_valid(&like, &expected_domain, false)?;
is_addressed_to_public(&like)?; verify_is_addressed_to_public(&like)?;
let object_id = like let object_id = like
.object() .object()
@ -384,14 +373,17 @@ pub(in crate::inbox) async fn receive_undo_like_for_community(
/// Add a new mod to the community (can only be done by an existing mod). /// Add a new mod to the community (can only be done by an existing mod).
pub(in crate::inbox) async fn receive_add_for_community( pub(in crate::inbox) async fn receive_add_for_community(
context: &LemmyContext, context: &LemmyContext,
activity: AnyBase, add_any_base: AnyBase,
expected_domain: &Url, announce: Option<Announce>,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let add = Add::from_any_base(activity.to_owned())?.context(location_info!())?; let add = Add::from_any_base(add_any_base.to_owned())?.context(location_info!())?;
verify_activity_domains_valid(&add, &expected_domain, false)?; let community = extract_community_from_cc(&add, context).await?;
is_addressed_to_public(&add)?;
let community = verify_actor_is_community_mod(&add, context).await?; verify_mod_activity(&add, announce, &community, context).await?;
verify_is_addressed_to_public(&add)?;
verify_actor_is_community_mod(&add, &community, context).await?;
verify_add_remove_moderator_target(&add, &community)?;
let new_mod = add let new_mod = add
.object() .object()
@ -417,7 +409,7 @@ pub(in crate::inbox) async fn receive_add_for_community(
.await??; .await??;
} }
if community.local { if community.local {
community.send_announce(activity, context).await?; community.send_announce(add_any_base, context).await?;
} }
// TODO: send websocket notification about added mod // TODO: send websocket notification about added mod
Ok(()) Ok(())
@ -433,7 +425,7 @@ pub(in crate::inbox) async fn receive_undo_dislike_for_community(
let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)? let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?; .context(location_info!())?;
verify_activity_domains_valid(&dislike, &expected_domain, false)?; verify_activity_domains_valid(&dislike, &expected_domain, false)?;
is_addressed_to_public(&dislike)?; verify_is_addressed_to_public(&dislike)?;
let object_id = dislike let object_id = dislike
.object() .object()
@ -465,26 +457,39 @@ async fn fetch_post_or_comment_by_id(
Err(NotFound.into()) Err(NotFound.into())
} }
async fn verify_actor_is_community_mod<T, Kind>( async fn extract_community_from_cc<T, Kind>(
activity: &T, activity: &T,
context: &LemmyContext, context: &LemmyContext,
) -> Result<Community, LemmyError> ) -> Result<Community, LemmyError>
where where
T: ActorAndObjectRef + BaseExt<Kind> + OptTargetRef, T: AsObject<Kind>,
{ {
// should be the moderators collection of a local community let cc = activity
let target = activity .cc()
.target() .map(|c| c.as_many())
.map(|t| t.as_single_xsd_any_uri())
.flatten() .flatten()
.context(location_info!())?; .context(location_info!())?;
// TODO: very hacky, we should probably store the moderators url in db let community_id = cc
let community_id: DbUrl = Url::parse(&target.to_string().replace("/moderators", ""))?.into(); .first()
.map(|c| c.as_xsd_any_uri())
.flatten()
.context(location_info!())?;
let community_id: DbUrl = community_id.to_owned().into();
let community = blocking(&context.pool(), move |conn| { let community = blocking(&context.pool(), move |conn| {
Community::read_from_apub_id(&conn, &community_id) Community::read_from_apub_id(&conn, &community_id)
}) })
.await??; .await??;
Ok(community)
}
async fn verify_actor_is_community_mod<T, Kind>(
activity: &T,
community: &Community,
context: &LemmyContext,
) -> Result<(), LemmyError>
where
T: ActorAndObjectRef + BaseExt<Kind>,
{
let actor = activity let actor = activity
.actor()? .actor()?
.as_single_xsd_any_uri() .as_single_xsd_any_uri()
@ -507,6 +512,43 @@ where
return Err(anyhow!("Not a mod").into()); return Err(anyhow!("Not a mod").into());
} }
// TODO: the function name doesnt make sense if we return the community Ok(())
Ok(community) }
async fn verify_mod_activity<T, Kind>(
mod_action: &T,
announce: Option<Announce>,
community: &Community,
context: &LemmyContext,
) -> Result<(), LemmyError>
where
T: ActorAndObjectRef + OptTargetRef + BaseExt<Kind>,
{
// Remove was sent by community to user, we just check that it came from the right domain
if let Some(announce) = announce {
verify_activity_domains_valid(&announce, &community.actor_id.to_owned().into(), false)?;
}
// Remove was sent by a remote mod to community, check that they are actually mod
else {
verify_actor_is_community_mod(mod_action, community, context).await?;
}
Ok(())
}
fn verify_add_remove_moderator_target<T, Kind>(
activity: &T,
community: &Community,
) -> Result<(), LemmyError>
where
T: ActorAndObjectRef + BaseExt<Kind> + OptTargetRef,
{
let target = activity
.target()
.map(|t| t.as_single_xsd_any_uri())
.flatten()
.context(location_info!())?;
if target != &generate_moderators_url(&community.actor_id)?.into_inner() {
return Err(anyhow!("Unkown target url").into());
}
Ok(())
} }

View File

@ -26,7 +26,6 @@ use crate::{
is_activity_already_known, is_activity_already_known,
is_addressed_to_community_followers, is_addressed_to_community_followers,
is_addressed_to_local_user, is_addressed_to_local_user,
is_addressed_to_public,
receive_for_community::{ receive_for_community::{
receive_add_for_community, receive_add_for_community,
receive_create_for_community, receive_create_for_community,
@ -37,6 +36,7 @@ use crate::{
receive_undo_for_community, receive_undo_for_community,
receive_update_for_community, receive_update_for_community,
}, },
verify_is_addressed_to_public,
}, },
insert_activity, insert_activity,
ActorType, ActorType,
@ -265,7 +265,7 @@ pub async fn receive_announce(
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let announce = Announce::from_any_base(activity)?.context(location_info!())?; let announce = Announce::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&announce, &actor.actor_id(), false)?; verify_activity_domains_valid(&announce, &actor.actor_id(), false)?;
is_addressed_to_public(&announce)?; verify_is_addressed_to_public(&announce)?;
let kind = announce let kind = announce
.object() .object()
@ -299,13 +299,13 @@ pub async fn receive_announce(
} }
Some(Delete) => receive_delete_for_community(context, inner_activity, &inner_id).await, Some(Delete) => receive_delete_for_community(context, inner_activity, &inner_id).await,
Some(Remove) => { Some(Remove) => {
receive_remove_for_community(context, inner_activity, &inner_id, request_counter).await receive_remove_for_community(context, inner_activity, Some(announce), request_counter).await
} }
Some(Undo) => { Some(Undo) => {
receive_undo_for_community(context, inner_activity, &inner_id, request_counter).await receive_undo_for_community(context, inner_activity, &inner_id, request_counter).await
} }
Some(Add) => { Some(Add) => {
receive_add_for_community(context, inner_activity, &inner_id, request_counter).await receive_add_for_community(context, inner_activity, Some(announce), request_counter).await
} }
_ => receive_unhandled_activity(inner_activity), _ => receive_unhandled_activity(inner_activity),
} }
@ -319,7 +319,7 @@ async fn receive_create(
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let create = Create::from_any_base(activity)?.context(location_info!())?; let create = Create::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&create, &expected_domain, true)?; verify_activity_domains_valid(&create, &expected_domain, true)?;
if is_addressed_to_public(&create).is_ok() { if verify_is_addressed_to_public(&create).is_ok() {
receive_create_comment(create, context, request_counter).await receive_create_comment(create, context, request_counter).await
} else { } else {
receive_create_private_message(&context, create, expected_domain, request_counter).await receive_create_private_message(&context, create, expected_domain, request_counter).await
@ -334,7 +334,7 @@ async fn receive_update(
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let update = Update::from_any_base(activity)?.context(location_info!())?; let update = Update::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&update, &expected_domain, true)?; verify_activity_domains_valid(&update, &expected_domain, true)?;
if is_addressed_to_public(&update).is_ok() { if verify_is_addressed_to_public(&update).is_ok() {
dessalines marked this conversation as resolved Outdated

This and the one below were wrong, we never receive a Create/Comment in the user inbox, its always wrapped in an announce.

This and the one below were wrong, we never receive a Create/Comment in the user inbox, its always wrapped in an announce.

Turns out I was wrong, we do receive mentions this way.

Turns out I was wrong, we do receive mentions this way.
receive_update_comment(update, context, request_counter).await receive_update_comment(update, context, request_counter).await
} else { } else {
receive_update_private_message(&context, update, expected_domain, request_counter).await receive_update_private_message(&context, update, expected_domain, request_counter).await