Merge pull request #1064 from LemmyNet/apub_security_checks

Apub security checks
This commit is contained in:
Dessalines 2020-08-07 11:33:58 -04:00 committed by GitHub
commit ded7650a60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 363 additions and 154 deletions

View file

@ -68,3 +68,16 @@ cd /lemmy/
sudo docker-compose pull
sudo docker-compose up -d
```
## Security Model
- HTTP signature verify: This ensures that activity really comes from the activity that it claims
- check_is_apub_valid : Makes sure its in our allowed instances list
- Lower level checks: To make sure that the user that creates/updates/removes a post is actually on the same instance as that post
For the last point, note that we are *not* checking whether the actor that sends the create activity for a post is
actually identical to the post's creator, or that the user that removes a post is a mod/admin. These things are checked
by the API code, and its the responsibility of each instance to check user permissions. This does not leave any attack
vector, as a normal instance user cant do actions that violate the API rules. The only one who could do that is the
admin (and the software deployed by the admin). But the admin can do anything on the instance, including send activities
from other user accounts. So we wouldnt actually gain any security by checking mod permissions or similar.

View file

@ -55,7 +55,7 @@ pub trait Perform {
) -> Result<Self::Response, LemmyError>;
}
pub async fn is_mod_or_admin(
pub(in crate::api) async fn is_mod_or_admin(
pool: &DbPool,
user_id: i32,
community_id: i32,
@ -65,7 +65,7 @@ pub async fn is_mod_or_admin(
})
.await?;
if !is_mod_or_admin {
return Err(APIError::err("not_an_admin").into());
return Err(APIError::err("not_a_mod_or_admin").into());
}
Ok(())
}
@ -104,14 +104,14 @@ pub(in crate::api) async fn get_user_from_jwt_opt(
}
}
pub(in crate::api) fn check_slurs(text: &str) -> Result<(), APIError> {
pub(in crate) fn check_slurs(text: &str) -> Result<(), APIError> {
if let Err(slurs) = slur_check(text) {
Err(APIError::err(&slurs_vec_to_str(slurs)))
} else {
Ok(())
}
}
pub(in crate::api) fn check_slurs_opt(text: &Option<String>) -> Result<(), APIError> {
pub(in crate) fn check_slurs_opt(text: &Option<String>) -> Result<(), APIError> {
match text {
Some(t) => check_slurs(t),
None => Ok(()),

View file

@ -1,6 +1,7 @@
use crate::{
apub::{
activities::{generate_activity_id, send_activity_to_community},
check_actor_domain,
create_apub_response,
create_apub_tombstone_response,
create_tombstone,
@ -48,7 +49,7 @@ use lemmy_db::{
user::User_,
Crud,
};
use lemmy_utils::{convert_datetime, scrape_text_for_mentions, MentionData};
use lemmy_utils::{convert_datetime, remove_slurs, scrape_text_for_mentions, MentionData};
use log::debug;
use serde::Deserialize;
use serde_json::Error;
@ -131,6 +132,7 @@ impl FromApub for CommentForm {
note: &Note,
client: &Client,
pool: &DbPool,
expected_domain: Option<Url>,
) -> Result<CommentForm, LemmyError> {
let creator_actor_id = &note
.attributed_to()
@ -165,23 +167,25 @@ impl FromApub for CommentForm {
}
None => None,
};
let content = note
.content()
.unwrap()
.as_single_xsd_string()
.unwrap()
.to_string();
let content_slurs_removed = remove_slurs(&content);
Ok(CommentForm {
creator_id: creator.id,
post_id: post.id,
parent_id,
content: note
.content()
.unwrap()
.as_single_xsd_string()
.unwrap()
.to_string(),
content: content_slurs_removed,
removed: None,
read: None,
published: note.published().map(|u| u.to_owned().naive_local()),
updated: note.updated().map(|u| u.to_owned().naive_local()),
deleted: None,
ap_id: note.id_unchecked().unwrap().to_string(),
ap_id: check_actor_domain(note, expected_domain)?,
local: false,
})
}

View file

@ -1,6 +1,8 @@
use crate::{
api::{check_slurs, check_slurs_opt},
apub::{
activities::{generate_activity_id, send_activity},
check_actor_domain,
create_apub_response,
create_apub_tombstone_response,
create_tombstone,
@ -322,7 +324,12 @@ impl FromApub for CommunityForm {
type ApubType = GroupExt;
/// Parse an ActivityPub group received from another instance into a Lemmy community.
async fn from_apub(group: &GroupExt, client: &Client, pool: &DbPool) -> Result<Self, LemmyError> {
async fn from_apub(
group: &GroupExt,
client: &Client,
pool: &DbPool,
expected_domain: Option<Url>,
) -> Result<Self, LemmyError> {
let creator_and_moderator_uris = group.inner.attributed_to().unwrap();
let creator_uri = creator_and_moderator_uris
.as_many()
@ -334,6 +341,25 @@ impl FromApub for CommunityForm {
.unwrap();
let creator = get_or_fetch_and_upsert_user(creator_uri, client, pool).await?;
let name = group
.inner
.name()
.unwrap()
.as_one()
.unwrap()
.as_xsd_string()
.unwrap()
.to_string();
let title = group.inner.preferred_username().unwrap().to_string();
// TODO: should be parsed as html and tags like <script> removed (or use markdown source)
// -> same for post.content etc
let description = group
.inner
.content()
.map(|s| s.as_single_xsd_string().unwrap().into());
check_slurs(&name)?;
check_slurs(&title)?;
check_slurs_opt(&description)?;
let icon = match group.icon() {
Some(any_image) => Some(
@ -362,22 +388,9 @@ impl FromApub for CommunityForm {
};
Ok(CommunityForm {
name: group
.inner
.name()
.unwrap()
.as_one()
.unwrap()
.as_xsd_string()
.unwrap()
.into(),
title: group.inner.preferred_username().unwrap().to_string(),
// TODO: should be parsed as html and tags like <script> removed (or use markdown source)
// -> same for post.content etc
description: group
.inner
.content()
.map(|s| s.as_single_xsd_string().unwrap().into()),
name,
title,
description,
category_id: group.ext_one.category.identifier.parse::<i32>()?,
creator_id: creator.id,
removed: None,
@ -385,7 +398,7 @@ impl FromApub for CommunityForm {
updated: group.inner.updated().map(|u| u.to_owned().naive_local()),
deleted: None,
nsfw: group.ext_one.sensitive,
actor_id: group.inner.id_unchecked().unwrap().to_string(),
actor_id: check_actor_domain(group, expected_domain)?,
local: false,
private_key: None,
public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
@ -486,12 +499,13 @@ pub async fn do_announce(
insert_activity(community.creator_id, announce.clone(), true, pool).await?;
// dont send to the instance where the activity originally came from, because that would result
// in a database error (same data inserted twice)
let mut to = community.get_follower_inboxes(pool).await?;
// dont send to the local instance, nor to the instance where the activity originally came from,
// because that would result in a database error (same data inserted twice)
// this seems to be the "easiest" stable alternative for remove_item()
to.retain(|x| *x != sender.get_shared_inbox_url());
to.retain(|x| *x != community.get_shared_inbox_url());
send_activity(client, &announce.into_any_base()?, community, to).await?;

View file

@ -172,7 +172,7 @@ pub async fn search_by_apub_id(
response
}
SearchAcceptedObjects::Page(p) => {
let post_form = PostForm::from_apub(&p, client, pool).await?;
let post_form = PostForm::from_apub(&p, client, pool, Some(query_url)).await?;
let p = blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
response.posts = vec![blocking(pool, move |conn| PostView::read(conn, p.id, None)).await??];
@ -185,8 +185,8 @@ pub async fn search_by_apub_id(
// TODO: also fetch parent comments if any
let x = post_url.first().unwrap().as_xsd_any_uri().unwrap();
let post = fetch_remote_object(client, x).await?;
let post_form = PostForm::from_apub(&post, client, pool).await?;
let comment_form = CommentForm::from_apub(&c, client, pool).await?;
let post_form = PostForm::from_apub(&post, client, pool, Some(query_url.clone())).await?;
let comment_form = CommentForm::from_apub(&c, client, pool, Some(query_url)).await?;
blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
let c = blocking(pool, move |conn| upsert_comment(&comment_form, conn)).await??;
@ -221,7 +221,7 @@ pub async fn get_or_fetch_and_upsert_user(
) -> Result<User_, LemmyError> {
let apub_id_owned = apub_id.to_owned();
let user = blocking(pool, move |conn| {
User_::read_from_actor_id(conn, apub_id_owned.as_str())
User_::read_from_actor_id(conn, apub_id_owned.as_ref())
})
.await?;
@ -231,7 +231,7 @@ pub async fn get_or_fetch_and_upsert_user(
debug!("Fetching and updating from remote user: {}", apub_id);
let person = fetch_remote_object::<PersonExt>(client, apub_id).await?;
let mut uf = UserForm::from_apub(&person, client, pool).await?;
let mut uf = UserForm::from_apub(&person, client, pool, Some(apub_id.to_owned())).await?;
uf.last_refreshed_at = Some(naive_now());
let user = blocking(pool, move |conn| User_::update(conn, u.id, &uf)).await??;
@ -242,7 +242,7 @@ pub async fn get_or_fetch_and_upsert_user(
debug!("Fetching and creating remote user: {}", apub_id);
let person = fetch_remote_object::<PersonExt>(client, apub_id).await?;
let uf = UserForm::from_apub(&person, client, pool).await?;
let uf = UserForm::from_apub(&person, client, pool, Some(apub_id.to_owned())).await?;
let user = blocking(pool, move |conn| User_::create(conn, &uf)).await??;
Ok(user)
@ -300,7 +300,7 @@ async fn fetch_remote_community(
) -> Result<Community, LemmyError> {
let group = fetch_remote_object::<GroupExt>(client, apub_id).await?;
let cf = CommunityForm::from_apub(&group, client, pool).await?;
let cf = CommunityForm::from_apub(&group, client, pool, Some(apub_id.to_owned())).await?;
let community = blocking(pool, move |conn| {
if let Some(cid) = community_id {
Community::update(conn, cid, &cf)
@ -350,7 +350,7 @@ async fn fetch_remote_community(
let outbox_items = outbox.items().unwrap().clone();
for o in outbox_items.many().unwrap() {
let page = PageExt::from_any_base(o)?.unwrap();
let post = PostForm::from_apub(&page, client, pool).await?;
let post = PostForm::from_apub(&page, client, pool, None).await?;
let post_ap_id = post.ap_id.clone();
// Check whether the post already exists in the local db
let existing = blocking(pool, move |conn| Post::read_from_apub_id(conn, &post_ap_id)).await?;
@ -358,6 +358,7 @@ async fn fetch_remote_community(
Ok(e) => blocking(pool, move |conn| Post::update(conn, e.id, &post)).await??,
Err(_) => blocking(pool, move |conn| Post::create(conn, &post)).await??,
};
// TODO: we need to send a websocket update here
}
Ok(community)
@ -388,7 +389,7 @@ pub async fn get_or_fetch_and_insert_post(
Err(NotFound {}) => {
debug!("Fetching and creating remote post: {}", post_ap_id);
let post = fetch_remote_object::<PageExt>(client, post_ap_id).await?;
let post_form = PostForm::from_apub(&post, client, pool).await?;
let post_form = PostForm::from_apub(&post, client, pool, Some(post_ap_id.to_owned())).await?;
let post = blocking(pool, move |conn| Post::create(conn, &post_form)).await??;
@ -426,7 +427,8 @@ pub async fn get_or_fetch_and_insert_comment(
comment_ap_id
);
let comment = fetch_remote_object::<Note>(client, comment_ap_id).await?;
let comment_form = CommentForm::from_apub(&comment, client, pool).await?;
let comment_form =
CommentForm::from_apub(&comment, client, pool, Some(comment_ap_id.to_owned())).await?;
let comment = blocking(pool, move |conn| Comment::create(conn, &comment_form)).await??;

View file

@ -9,13 +9,17 @@ use crate::{
undo::receive_undo,
update::receive_update,
},
shared_inbox::receive_unhandled_activity,
shared_inbox::{get_community_id_from_activity, receive_unhandled_activity},
},
routes::ChatServerParam,
DbPool,
LemmyError,
};
use activitystreams::{activity::*, base::AnyBase, prelude::ExtendsExt};
use activitystreams::{
activity::*,
base::{AnyBase, BaseExt},
prelude::ExtendsExt,
};
use actix_web::{client::Client, HttpResponse};
pub async fn receive_announce(
@ -25,6 +29,11 @@ pub async fn receive_announce(
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let announce = Announce::from_any_base(activity)?.unwrap();
// ensure that announce and community come from the same instance
let community = get_community_id_from_activity(&announce)?;
announce.id(community.domain().unwrap())?;
let kind = announce.object().as_single_kind_str();
let object = announce.object();
let object2 = object.clone().one().unwrap();

View file

@ -9,6 +9,7 @@ use crate::{
get_user_from_activity,
receive_unhandled_activity,
},
ActorType,
FromApub,
PageExt,
},
@ -39,6 +40,11 @@ pub async fn receive_create(
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let create = Create::from_any_base(activity)?.unwrap();
// ensure that create and actor come from the same instance
let user = get_user_from_activity(&create, client, pool).await?;
create.id(user.actor_id()?.domain().unwrap())?;
match create.object().as_single_kind_str() {
Some("Page") => receive_create_post(create, client, pool, chat_server).await,
Some("Note") => receive_create_comment(create, client, pool, chat_server).await,
@ -55,7 +61,7 @@ async fn receive_create_post(
let user = get_user_from_activity(&create, client, pool).await?;
let page = PageExt::from_any_base(create.object().to_owned().one().unwrap())?.unwrap();
let post = PostForm::from_apub(&page, client, pool).await?;
let post = PostForm::from_apub(&page, client, pool, Some(user.actor_id()?)).await?;
let inserted_post = blocking(pool, move |conn| Post::create(conn, &post)).await??;
@ -87,7 +93,7 @@ async fn receive_create_comment(
let user = get_user_from_activity(&create, client, pool).await?;
let note = Note::from_any_base(create.object().to_owned().one().unwrap())?.unwrap();
let comment = CommentForm::from_apub(&note, client, pool).await?;
let comment = CommentForm::from_apub(&note, client, pool, Some(user.actor_id()?)).await?;
let inserted_comment = blocking(pool, move |conn| Comment::create(conn, &comment)).await??;

View file

@ -7,6 +7,7 @@ use crate::{
get_user_from_activity,
receive_unhandled_activity,
},
ActorType,
FromApub,
GroupExt,
PageExt,
@ -57,7 +58,7 @@ async fn receive_delete_post(
let user = get_user_from_activity(&delete, client, pool).await?;
let page = PageExt::from_any_base(delete.object().to_owned().one().unwrap())?.unwrap();
let post_ap_id = PostForm::from_apub(&page, client, pool)
let post_ap_id = PostForm::from_apub(&page, client, pool, Some(user.actor_id()?))
.await?
.get_ap_id()?;
@ -111,7 +112,7 @@ async fn receive_delete_comment(
let user = get_user_from_activity(&delete, client, pool).await?;
let note = Note::from_any_base(delete.object().to_owned().one().unwrap())?.unwrap();
let comment_ap_id = CommentForm::from_apub(&note, client, pool)
let comment_ap_id = CommentForm::from_apub(&note, client, pool, Some(user.actor_id()?))
.await?
.get_ap_id()?;
@ -168,7 +169,7 @@ async fn receive_delete_community(
let group = GroupExt::from_any_base(delete.object().to_owned().one().unwrap())?.unwrap();
let user = get_user_from_activity(&delete, client, pool).await?;
let community_actor_id = CommunityForm::from_apub(&group, client, pool)
let community_actor_id = CommunityForm::from_apub(&group, client, pool, Some(user.actor_id()?))
.await?
.actor_id;

View file

@ -52,7 +52,7 @@ async fn receive_dislike_post(
let user = get_user_from_activity(&dislike, client, pool).await?;
let page = PageExt::from_any_base(dislike.object().to_owned().one().unwrap())?.unwrap();
let post = PostForm::from_apub(&page, client, pool).await?;
let post = PostForm::from_apub(&page, client, pool, None).await?;
let post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, client, pool)
.await?
@ -93,7 +93,7 @@ async fn receive_dislike_comment(
let note = Note::from_any_base(dislike.object().to_owned().one().unwrap())?.unwrap();
let user = get_user_from_activity(&dislike, client, pool).await?;
let comment = CommentForm::from_apub(&note, client, pool).await?;
let comment = CommentForm::from_apub(&note, client, pool, None).await?;
let comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, client, pool)
.await?

View file

@ -52,7 +52,7 @@ async fn receive_like_post(
let user = get_user_from_activity(&like, client, pool).await?;
let page = PageExt::from_any_base(like.object().to_owned().one().unwrap())?.unwrap();
let post = PostForm::from_apub(&page, client, pool).await?;
let post = PostForm::from_apub(&page, client, pool, None).await?;
let post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, client, pool)
.await?
@ -93,7 +93,7 @@ async fn receive_like_comment(
let note = Note::from_any_base(like.object().to_owned().one().unwrap())?.unwrap();
let user = get_user_from_activity(&like, client, pool).await?;
let comment = CommentForm::from_apub(&note, client, pool).await?;
let comment = CommentForm::from_apub(&note, client, pool, None).await?;
let comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, client, pool)
.await?

View file

@ -4,9 +4,11 @@ use crate::{
fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
inbox::shared_inbox::{
announce_if_community_is_local,
get_community_id_from_activity,
get_user_from_activity,
receive_unhandled_activity,
},
ActorType,
FromApub,
GroupExt,
PageExt,
@ -22,6 +24,7 @@ use crate::{
};
use activitystreams::{activity::Remove, base::AnyBase, object::Note, prelude::*};
use actix_web::{client::Client, HttpResponse};
use anyhow::anyhow;
use lemmy_db::{
comment::{Comment, CommentForm},
comment_view::CommentView,
@ -40,6 +43,12 @@ pub async fn receive_remove(
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let remove = Remove::from_any_base(activity)?.unwrap();
let actor = get_user_from_activity(&remove, client, pool).await?;
let community = get_community_id_from_activity(&remove)?;
if actor.actor_id()?.domain() != community.domain() {
return Err(anyhow!("Remove activities are only allowed on local objects").into());
}
match remove.object().as_single_kind_str() {
Some("Page") => receive_remove_post(remove, client, pool, chat_server).await,
Some("Note") => receive_remove_comment(remove, client, pool, chat_server).await,
@ -57,7 +66,7 @@ async fn receive_remove_post(
let mod_ = get_user_from_activity(&remove, client, pool).await?;
let page = PageExt::from_any_base(remove.object().to_owned().one().unwrap())?.unwrap();
let post_ap_id = PostForm::from_apub(&page, client, pool)
let post_ap_id = PostForm::from_apub(&page, client, pool, None)
.await?
.get_ap_id()?;
@ -111,7 +120,7 @@ async fn receive_remove_comment(
let mod_ = get_user_from_activity(&remove, client, pool).await?;
let note = Note::from_any_base(remove.object().to_owned().one().unwrap())?.unwrap();
let comment_ap_id = CommentForm::from_apub(&note, client, pool)
let comment_ap_id = CommentForm::from_apub(&note, client, pool, None)
.await?
.get_ap_id()?;
@ -168,7 +177,7 @@ async fn receive_remove_community(
let mod_ = get_user_from_activity(&remove, client, pool).await?;
let group = GroupExt::from_any_base(remove.object().to_owned().one().unwrap())?.unwrap();
let community_actor_id = CommunityForm::from_apub(&group, client, pool)
let community_actor_id = CommunityForm::from_apub(&group, client, pool, Some(mod_.actor_id()?))
.await?
.actor_id;

View file

@ -7,6 +7,7 @@ use crate::{
get_user_from_activity,
receive_unhandled_activity,
},
ActorType,
FromApub,
GroupExt,
PageExt,
@ -20,7 +21,12 @@ use crate::{
DbPool,
LemmyError,
};
use activitystreams::{activity::*, base::AnyBase, object::Note, prelude::*};
use activitystreams::{
activity::*,
base::{AnyBase, AsBase},
object::Note,
prelude::*,
};
use actix_web::{client::Client, HttpResponse};
use anyhow::anyhow;
use lemmy_db::{
@ -47,11 +53,27 @@ pub async fn receive_undo(
Some("Remove") => receive_undo_remove(undo, client, pool, chat_server).await,
Some("Like") => receive_undo_like(undo, client, pool, chat_server).await,
Some("Dislike") => receive_undo_dislike(undo, client, pool, chat_server).await,
// TODO: handle undo_dislike?
_ => receive_unhandled_activity(undo),
}
}
fn check_is_undo_valid<T, A>(outer_activity: &Undo, inner_activity: &T) -> Result<(), LemmyError>
where
T: AsBase<A> + ActorAndObjectRef,
{
let outer_actor = outer_activity.actor()?;
let outer_actor_uri = outer_actor.as_single_xsd_any_uri().unwrap();
let inner_actor = inner_activity.actor()?;
let inner_actor_uri = inner_actor.as_single_xsd_any_uri().unwrap();
if outer_actor_uri.domain() != inner_actor_uri.domain() {
Err(anyhow!("Cant undo activities from a different instance").into())
} else {
Ok(())
}
}
async fn receive_undo_delete(
undo: Undo,
client: &Client,
@ -59,6 +81,7 @@ async fn receive_undo_delete(
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let delete = Delete::from_any_base(undo.object().to_owned().one().unwrap())?.unwrap();
check_is_undo_valid(&undo, &delete)?;
let type_ = delete.object().as_single_kind_str().unwrap();
match type_ {
"Note" => receive_undo_delete_comment(undo, &delete, client, pool, chat_server).await,
@ -75,6 +98,7 @@ async fn receive_undo_remove(
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let remove = Remove::from_any_base(undo.object().to_owned().one().unwrap())?.unwrap();
check_is_undo_valid(&undo, &remove)?;
let type_ = remove.object().as_single_kind_str().unwrap();
match type_ {
@ -92,6 +116,7 @@ async fn receive_undo_like(
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let like = Like::from_any_base(undo.object().to_owned().one().unwrap())?.unwrap();
check_is_undo_valid(&undo, &like)?;
let type_ = like.object().as_single_kind_str().unwrap();
match type_ {
@ -108,6 +133,9 @@ async fn receive_undo_dislike(
_chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let dislike = Dislike::from_any_base(undo.object().to_owned().one().unwrap())?.unwrap();
check_is_undo_valid(&undo, &dislike)?;
// TODO: need to implement Undo<Dislike>
let type_ = dislike.object().as_single_kind_str().unwrap();
Err(anyhow!("Undo Delete type {} not supported", type_).into())
@ -123,7 +151,7 @@ async fn receive_undo_delete_comment(
let user = get_user_from_activity(delete, client, pool).await?;
let note = Note::from_any_base(delete.object().to_owned().one().unwrap())?.unwrap();
let comment_ap_id = CommentForm::from_apub(&note, client, pool)
let comment_ap_id = CommentForm::from_apub(&note, client, pool, Some(user.actor_id()?))
.await?
.get_ap_id()?;
@ -181,7 +209,7 @@ async fn receive_undo_remove_comment(
let mod_ = get_user_from_activity(remove, client, pool).await?;
let note = Note::from_any_base(remove.object().to_owned().one().unwrap())?.unwrap();
let comment_ap_id = CommentForm::from_apub(&note, client, pool)
let comment_ap_id = CommentForm::from_apub(&note, client, pool, None)
.await?
.get_ap_id()?;
@ -239,7 +267,7 @@ async fn receive_undo_delete_post(
let user = get_user_from_activity(delete, client, pool).await?;
let page = PageExt::from_any_base(delete.object().to_owned().one().unwrap())?.unwrap();
let post_ap_id = PostForm::from_apub(&page, client, pool)
let post_ap_id = PostForm::from_apub(&page, client, pool, Some(user.actor_id()?))
.await?
.get_ap_id()?;
@ -294,7 +322,7 @@ async fn receive_undo_remove_post(
let mod_ = get_user_from_activity(remove, client, pool).await?;
let page = PageExt::from_any_base(remove.object().to_owned().one().unwrap())?.unwrap();
let post_ap_id = PostForm::from_apub(&page, client, pool)
let post_ap_id = PostForm::from_apub(&page, client, pool, None)
.await?
.get_ap_id()?;
@ -349,7 +377,7 @@ async fn receive_undo_delete_community(
let user = get_user_from_activity(delete, client, pool).await?;
let group = GroupExt::from_any_base(delete.object().to_owned().one().unwrap())?.unwrap();
let community_actor_id = CommunityForm::from_apub(&group, client, pool)
let community_actor_id = CommunityForm::from_apub(&group, client, pool, Some(user.actor_id()?))
.await?
.actor_id;
@ -415,7 +443,7 @@ async fn receive_undo_remove_community(
let mod_ = get_user_from_activity(remove, client, pool).await?;
let group = GroupExt::from_any_base(remove.object().to_owned().one().unwrap())?.unwrap();
let community_actor_id = CommunityForm::from_apub(&group, client, pool)
let community_actor_id = CommunityForm::from_apub(&group, client, pool, Some(mod_.actor_id()?))
.await?
.actor_id;
@ -481,7 +509,7 @@ async fn receive_undo_like_comment(
let user = get_user_from_activity(like, client, pool).await?;
let note = Note::from_any_base(like.object().to_owned().one().unwrap())?.unwrap();
let comment = CommentForm::from_apub(&note, client, pool).await?;
let comment = CommentForm::from_apub(&note, client, pool, None).await?;
let comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, client, pool)
.await?
@ -527,7 +555,7 @@ async fn receive_undo_like_post(
let user = get_user_from_activity(like, client, pool).await?;
let page = PageExt::from_any_base(like.object().to_owned().one().unwrap())?.unwrap();
let post = PostForm::from_apub(&page, client, pool).await?;
let post = PostForm::from_apub(&page, client, pool, None).await?;
let post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, client, pool)
.await?

View file

@ -10,6 +10,7 @@ use crate::{
get_user_from_activity,
receive_unhandled_activity,
},
ActorType,
FromApub,
PageExt,
},
@ -40,6 +41,11 @@ pub async fn receive_update(
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let update = Update::from_any_base(activity)?.unwrap();
// ensure that update and actor come from the same instance
let user = get_user_from_activity(&update, client, pool).await?;
update.id(user.actor_id()?.domain().unwrap())?;
match update.object().as_single_kind_str() {
Some("Page") => receive_update_post(update, client, pool, chat_server).await,
Some("Note") => receive_update_comment(update, client, pool, chat_server).await,
@ -56,16 +62,22 @@ async fn receive_update_post(
let user = get_user_from_activity(&update, client, pool).await?;
let page = PageExt::from_any_base(update.object().to_owned().one().unwrap())?.unwrap();
let post = PostForm::from_apub(&page, client, pool).await?;
let post = PostForm::from_apub(&page, client, pool, Some(user.actor_id()?)).await?;
let post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, client, pool)
let original_post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, client, pool)
.await?
.id;
blocking(pool, move |conn| Post::update(conn, post_id, &post)).await??;
blocking(pool, move |conn| {
Post::update(conn, original_post_id, &post)
})
.await??;
// Refetch the view
let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??;
let post_view = blocking(pool, move |conn| {
PostView::read(conn, original_post_id, None)
})
.await??;
let res = PostResponse { post: post_view };
@ -88,14 +100,14 @@ async fn receive_update_comment(
let note = Note::from_any_base(update.object().to_owned().one().unwrap())?.unwrap();
let user = get_user_from_activity(&update, client, pool).await?;
let comment = CommentForm::from_apub(&note, client, pool).await?;
let comment = CommentForm::from_apub(&note, client, pool, Some(user.actor_id()?)).await?;
let comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, client, pool)
let original_comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, client, pool)
.await?
.id;
let updated_comment = blocking(pool, move |conn| {
Comment::update(conn, comment_id, &comment)
Comment::update(conn, original_comment_id, &comment)
})
.await??;
@ -107,8 +119,10 @@ async fn receive_update_comment(
send_local_notifs(mentions, updated_comment, &user, post, pool, false).await?;
// Refetch the view
let comment_view =
blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??;
let comment_view = blocking(pool, move |conn| {
CommentView::read(conn, original_comment_id, None)
})
.await??;
let res = CommentResponse {
comment: comment_view,

View file

@ -69,14 +69,16 @@ pub async fn community_inbox(
verify(&request, &user)?;
insert_activity(user.id, activity.clone(), false, &db).await?;
let any_base = activity.clone().into_any_base()?;
let kind = activity.kind().unwrap();
match kind {
ValidTypes::Follow => handle_follow(any_base, user, community, &client, db).await,
ValidTypes::Undo => handle_undo_follow(any_base, user, community, db).await,
}
let user_id = user.id;
let res = match kind {
ValidTypes::Follow => handle_follow(any_base, user, community, &client, &db).await,
ValidTypes::Undo => handle_undo_follow(any_base, user, community, &db).await,
};
insert_activity(user_id, activity.clone(), false, &db).await?;
res
}
/// Handle a follow request from a remote user, adding it to the local database and returning an
@ -86,7 +88,7 @@ async fn handle_follow(
user: User_,
community: Community,
client: &Client,
db: DbPoolParam,
db: &DbPoolParam,
) -> Result<HttpResponse, LemmyError> {
let follow = Follow::from_any_base(activity)?.unwrap();
let community_follower_form = CommunityFollowerForm {
@ -95,12 +97,12 @@ async fn handle_follow(
};
// This will fail if they're already a follower, but ignore the error.
blocking(&db, move |conn| {
blocking(db, move |conn| {
CommunityFollower::follow(&conn, &community_follower_form).ok()
})
.await?;
community.send_accept_follow(follow, &client, &db).await?;
community.send_accept_follow(follow, &client, db).await?;
Ok(HttpResponse::Ok().finish())
}
@ -109,7 +111,7 @@ async fn handle_undo_follow(
activity: AnyBase,
user: User_,
community: Community,
db: DbPoolParam,
db: &DbPoolParam,
) -> Result<HttpResponse, LemmyError> {
let _undo = Undo::from_any_base(activity)?.unwrap();
@ -119,7 +121,7 @@ async fn handle_undo_follow(
};
// This will fail if they aren't a follower, but ignore the error.
blocking(&db, move |conn| {
blocking(db, move |conn| {
CommunityFollower::unfollow(&conn, &community_follower_form).ok()
})
.await?;

View file

@ -68,20 +68,17 @@ pub async fn shared_inbox(
debug!("Shared inbox received activity: {}", json);
let sender = &activity.actor()?.to_owned().single_xsd_any_uri().unwrap();
// TODO: pass this actor in instead of using get_user_from_activity()
let actor = get_or_fetch_and_upsert_actor(sender, &client, &pool).await?;
let community = get_community_id_from_activity(&activity).await;
let community = get_community_id_from_activity(&activity)?;
check_is_apub_id_valid(sender)?;
check_is_apub_id_valid(&community)?;
verify(&request, actor.as_ref())?;
insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
let actor = get_or_fetch_and_upsert_actor(sender, &client, &pool).await?;
verify(&request, actor.as_ref())?;
let any_base = activity.clone().into_any_base()?;
let kind = activity.kind().unwrap();
match kind {
let res = match kind {
ValidTypes::Announce => receive_announce(any_base, &client, &pool, chat_server).await,
ValidTypes::Create => receive_create(any_base, &client, &pool, chat_server).await,
ValidTypes::Update => receive_update(any_base, &client, &pool, chat_server).await,
@ -90,7 +87,10 @@ pub async fn shared_inbox(
ValidTypes::Remove => receive_remove(any_base, &client, &pool, chat_server).await,
ValidTypes::Delete => receive_delete(any_base, &client, &pool, chat_server).await,
ValidTypes::Undo => receive_undo(any_base, &client, &pool, chat_server).await,
}
};
insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
res
}
pub(in crate::apub::inbox) fn receive_unhandled_activity<A>(
@ -116,13 +116,15 @@ where
get_or_fetch_and_upsert_user(&user_uri, client, pool).await
}
pub(in crate::apub::inbox) async fn get_community_id_from_activity<T, A>(activity: &T) -> Url
pub(in crate::apub::inbox) fn get_community_id_from_activity<T, A>(
activity: &T,
) -> Result<Url, LemmyError>
where
T: AsBase<A> + ActorAndObjectRef + AsObject<A>,
{
let cc = activity.cc().unwrap();
let cc = cc.as_many().unwrap();
cc.first().unwrap().as_xsd_any_uri().unwrap().to_owned()
Ok(cc.first().unwrap().as_xsd_any_uri().unwrap().to_owned())
}
pub(in crate::apub::inbox) async fn announce_if_community_is_local<T, Kind>(

View file

@ -65,11 +65,9 @@ pub async fn user_inbox(
let actor = get_or_fetch_and_upsert_actor(actor_uri, &client, &pool).await?;
verify(&request, actor.as_ref())?;
insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
let any_base = activity.clone().into_any_base()?;
let kind = activity.kind().unwrap();
match kind {
let res = match kind {
ValidTypes::Accept => receive_accept(any_base, username, &client, &pool).await,
ValidTypes::Create => {
receive_create_private_message(any_base, &client, &pool, chat_server).await
@ -83,7 +81,10 @@ pub async fn user_inbox(
ValidTypes::Undo => {
receive_undo_delete_private_message(any_base, &client, &pool, chat_server).await
}
}
};
insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
res
}
/// Handle accepted follows.
@ -125,7 +126,8 @@ async fn receive_create_private_message(
let create = Create::from_any_base(activity)?.unwrap();
let note = Note::from_any_base(create.object().as_one().unwrap().to_owned())?.unwrap();
let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?;
let domain = Some(create.id_unchecked().unwrap().to_owned());
let private_message = PrivateMessageForm::from_apub(&note, client, pool, domain).await?;
let inserted_private_message = blocking(pool, move |conn| {
PrivateMessage::create(conn, &private_message)
@ -160,7 +162,8 @@ async fn receive_update_private_message(
let update = Update::from_any_base(activity)?.unwrap();
let note = Note::from_any_base(update.object().as_one().unwrap().to_owned())?.unwrap();
let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?;
let domain = Some(update.id_unchecked().unwrap().to_owned());
let private_message_form = PrivateMessageForm::from_apub(&note, client, pool, domain).await?;
let private_message_ap_id = private_message_form.ap_id.clone();
let private_message = blocking(pool, move |conn| {
@ -203,7 +206,8 @@ async fn receive_delete_private_message(
let delete = Delete::from_any_base(activity)?.unwrap();
let note = Note::from_any_base(delete.object().as_one().unwrap().to_owned())?.unwrap();
let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?;
let domain = Some(delete.id_unchecked().unwrap().to_owned());
let private_message_form = PrivateMessageForm::from_apub(&note, client, pool, domain).await?;
let private_message_ap_id = private_message_form.ap_id;
let private_message = blocking(pool, move |conn| {
@ -259,7 +263,8 @@ async fn receive_undo_delete_private_message(
let delete = Delete::from_any_base(undo.object().as_one().unwrap().to_owned())?.unwrap();
let note = Note::from_any_base(delete.object().as_one().unwrap().to_owned())?.unwrap();
let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?;
let domain = Some(undo.id_unchecked().unwrap().to_owned());
let private_message = PrivateMessageForm::from_apub(&note, client, pool, domain).await?;
let private_message_ap_id = private_message.ap_id.clone();
let private_message_id = blocking(pool, move |conn| {

View file

@ -23,6 +23,8 @@ use crate::{
use activitystreams::{
activity::Follow,
actor::{ApActor, Group, Person},
base::AsBase,
markers::Base,
object::{Page, Tombstone},
prelude::*,
};
@ -129,10 +131,19 @@ where
#[async_trait::async_trait(?Send)]
pub trait FromApub {
type ApubType;
/// Converts an object from ActivityPub type to Lemmy internal type.
///
/// * `apub` The object to read from
/// * `client` Web client to fetch remote actors with
/// * `pool` Database connection
/// * `expected_domain` If present, ensure that the apub object comes from the same domain as
/// this URL
///
async fn from_apub(
apub: &Self::ApubType,
client: &Client,
pool: &DbPool,
expected_domain: Option<Url>,
) -> Result<Self, LemmyError>
where
Self: Sized;
@ -178,6 +189,24 @@ pub trait ApubObjectType {
) -> Result<(), LemmyError>;
}
pub(in crate::apub) fn check_actor_domain<T, Kind>(
apub: &T,
expected_domain: Option<Url>,
) -> Result<String, LemmyError>
where
T: Base + AsBase<Kind>,
{
let actor_id = if let Some(url) = expected_domain {
let domain = url.domain().unwrap();
apub.id(domain)?.unwrap()
} else {
let actor_id = apub.id_unchecked().unwrap();
check_is_apub_id_valid(&actor_id)?;
actor_id
};
Ok(actor_id.to_string())
}
#[async_trait::async_trait(?Send)]
pub trait ApubLikeableType {
async fn send_like(

View file

@ -1,6 +1,8 @@
use crate::{
api::check_slurs,
apub::{
activities::{generate_activity_id, send_activity_to_community},
check_actor_domain,
create_apub_response,
create_apub_tombstone_response,
create_tombstone,
@ -42,7 +44,7 @@ use lemmy_db::{
user::User_,
Crud,
};
use lemmy_utils::convert_datetime;
use lemmy_utils::{convert_datetime, remove_slurs};
use serde::Deserialize;
use url::Url;
@ -154,6 +156,7 @@ impl FromApub for PostForm {
page: &PageExt,
client: &Client,
pool: &DbPool,
expected_domain: Option<Url>,
) -> Result<PostForm, LemmyError> {
let ext = &page.ext_one;
let creator_actor_id = page
@ -203,6 +206,14 @@ impl FromApub for PostForm {
None => (None, None, None),
};
let name = page
.inner
.summary()
.as_ref()
.unwrap()
.as_single_xsd_string()
.unwrap()
.to_string();
let url = page
.inner
.url()
@ -213,17 +224,12 @@ impl FromApub for PostForm {
.content()
.as_ref()
.map(|c| c.as_single_xsd_string().unwrap().to_string());
check_slurs(&name)?;
let body_slurs_removed = body.map(|b| remove_slurs(&b));
Ok(PostForm {
name: page
.inner
.summary()
.as_ref()
.unwrap()
.as_single_xsd_string()
.unwrap()
.to_string(),
name,
url,
body,
body: body_slurs_removed,
creator_id: creator.id,
community_id: community.id,
removed: None,
@ -245,7 +251,7 @@ impl FromApub for PostForm {
embed_description,
embed_html,
thumbnail_url,
ap_id: page.inner.id_unchecked().unwrap().to_string(),
ap_id: check_actor_domain(page, expected_domain)?,
local: false,
})
}

View file

@ -1,6 +1,8 @@
use crate::{
apub::{
activities::{generate_activity_id, send_activity},
check_actor_domain,
check_is_apub_id_valid,
create_tombstone,
fetcher::get_or_fetch_and_upsert_user,
insert_activity,
@ -75,6 +77,7 @@ impl FromApub for PrivateMessageForm {
note: &Note,
client: &Client,
pool: &DbPool,
expected_domain: Option<Url>,
) -> Result<PrivateMessageForm, LemmyError> {
let creator_actor_id = note
.attributed_to()
@ -84,10 +87,10 @@ impl FromApub for PrivateMessageForm {
.unwrap();
let creator = get_or_fetch_and_upsert_user(&creator_actor_id, client, pool).await?;
let recipient_actor_id = note.to().unwrap().clone().single_xsd_any_uri().unwrap();
let recipient = get_or_fetch_and_upsert_user(&recipient_actor_id, client, pool).await?;
let ap_id = note.id_unchecked().unwrap().to_string();
check_is_apub_id_valid(&Url::parse(&ap_id)?)?;
Ok(PrivateMessageForm {
creator_id: creator.id,
@ -102,7 +105,7 @@ impl FromApub for PrivateMessageForm {
updated: note.updated().map(|u| u.to_owned().naive_local()),
deleted: None,
read: None,
ap_id: note.id_unchecked().unwrap().to_string(),
ap_id: check_actor_domain(note, expected_domain)?,
local: false,
})
}

View file

@ -1,6 +1,8 @@
use crate::{
api::{check_slurs, check_slurs_opt},
apub::{
activities::{generate_activity_id, send_activity},
check_actor_domain,
create_apub_response,
insert_activity,
ActorType,
@ -211,7 +213,12 @@ impl ActorType for User_ {
impl FromApub for UserForm {
type ApubType = PersonExt;
/// Parse an ActivityPub person received from another instance into a Lemmy user.
async fn from_apub(person: &PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> {
async fn from_apub(
person: &PersonExt,
_: &Client,
_: &DbPool,
expected_domain: Option<Url>,
) -> Result<Self, LemmyError> {
let avatar = match person.icon() {
Some(any_image) => Some(
Image::from_any_base(any_image.as_one().unwrap().clone())
@ -238,16 +245,26 @@ impl FromApub for UserForm {
None => None,
};
let name = person
.name()
.unwrap()
.one()
.unwrap()
.as_xsd_string()
.unwrap()
.to_string();
let preferred_username = person.inner.preferred_username().map(|u| u.to_string());
let bio = person
.inner
.summary()
.map(|s| s.as_single_xsd_string().unwrap().into());
check_slurs(&name)?;
check_slurs_opt(&preferred_username)?;
check_slurs_opt(&bio)?;
Ok(UserForm {
name: person
.name()
.unwrap()
.one()
.unwrap()
.as_xsd_string()
.unwrap()
.to_string(),
preferred_username: person.inner.preferred_username().map(|u| u.to_string()),
name,
preferred_username,
password_encrypted: "".to_string(),
admin: false,
banned: false,
@ -263,11 +280,8 @@ impl FromApub for UserForm {
show_avatars: false,
send_notifications_to_email: false,
matrix_user_id: None,
actor_id: person.id_unchecked().unwrap().to_string(),
bio: person
.inner
.summary()
.map(|s| s.as_single_xsd_string().unwrap().into()),
actor_id: check_actor_domain(person, expected_domain)?,
bio,
local: false,
private_key: None,
public_key: Some(person.ext_one.public_key.to_owned().public_key_pem),

View file

@ -16,6 +16,9 @@ import {
getMentions,
searchPost,
unfollowRemotes,
createCommunity,
registerUser,
API,
} from './shared';
import { PostResponse } from '../interfaces';
@ -102,31 +105,51 @@ test('Delete a comment', async () => {
expect(betaComment2.deleted).toBe(false);
});
test('Remove a comment', async () => {
test('Remove a comment from admin and community on the same instance', async () => {
let commentRes = await createComment(alpha, postRes.post.id);
let removeCommentRes = await removeComment(
alpha,
true,
commentRes.comment.id
);
// Get the id for beta
let betaCommentId = (await searchComment(beta, commentRes.comment))
.comments[0].id;
// The beta admin removes it (the community lives on beta)
let removeCommentRes = await removeComment(beta, true, betaCommentId);
expect(removeCommentRes.comment.removed).toBe(true);
// Make sure that comment is removed on beta
let searchBeta = await searchComment(beta, commentRes.comment);
let betaComment = searchBeta.comments[0];
expect(betaComment.removed).toBe(true);
// Make sure that comment is removed on alpha (it gets pushed since an admin from beta removed it)
let refetchedPost = await getPost(alpha, postRes.post.id);
expect(refetchedPost.comments[0].removed).toBe(true);
let unremoveCommentRes = await removeComment(
alpha,
false,
commentRes.comment.id
);
let unremoveCommentRes = await removeComment(beta, false, betaCommentId);
expect(unremoveCommentRes.comment.removed).toBe(false);
// Make sure that comment is unremoved on beta
let searchBeta2 = await searchComment(beta, commentRes.comment);
let betaComment2 = searchBeta2.comments[0];
expect(betaComment2.removed).toBe(false);
let refetchedPost2 = await getPost(alpha, postRes.post.id);
expect(refetchedPost2.comments[0].removed).toBe(false);
});
test('Remove a comment from admin and community on different instance', async () => {
let alphaUser = await registerUser(alpha);
let newAlphaApi: API = {
url: alpha.url,
auth: alphaUser.jwt,
};
// New alpha user creates a community, post, and comment.
let newCommunity = await createCommunity(newAlphaApi);
let newPost = await createPost(newAlphaApi, newCommunity.community.id);
let commentRes = await createComment(newAlphaApi, newPost.post.id);
expect(commentRes.comment.content).toBeDefined();
// Beta searches that to cache it, then removes it
let searchBeta = await searchComment(beta, commentRes.comment);
let betaComment = searchBeta.comments[0];
let removeCommentRes = await removeComment(beta, true, betaComment.id);
expect(removeCommentRes.comment.removed).toBe(true);
// Make sure its not removed on alpha
let refetchedPost = await getPost(newAlphaApi, newPost.post.id);
expect(refetchedPost.comments[0].removed).toBe(false);
});
test('Unlike a comment', async () => {

View file

@ -154,17 +154,17 @@ test('Delete a post', async () => {
expect(betaPost2.post.deleted).toBe(false);
});
test('Remove a post', async () => {
test('Remove a post from admin and community on different instance', async () => {
let search = await searchForBetaCommunity(alpha);
let postRes = await createPost(alpha, search.communities[0].id);
let removedPost = await removePost(alpha, true, postRes.post);
expect(removedPost.post.removed).toBe(true);
// Make sure lemmy beta sees post is removed
// Make sure lemmy beta sees post is NOT removed
let createFakeBetaPostToGetId = (await createPost(beta, 2)).post.id - 1;
let betaPost = await getPost(beta, createFakeBetaPostToGetId);
expect(betaPost.post.removed).toBe(true);
expect(betaPost.post.removed).toBe(false);
// Undelete
let undeletedPost = await removePost(alpha, false, postRes.post);
@ -175,6 +175,31 @@ test('Remove a post', async () => {
expect(betaPost2.post.removed).toBe(false);
});
test('Remove a post from admin and community on same instance', async () => {
let search = await searchForBetaCommunity(alpha);
let postRes = await createPost(alpha, search.communities[0].id);
// Get the id for beta
let createFakeBetaPostToGetId = (await createPost(beta, 2)).post.id - 1;
let betaPost = await getPost(beta, createFakeBetaPostToGetId);
// The beta admin removes it (the community lives on beta)
let removePostRes = await removePost(beta, true, betaPost.post);
expect(removePostRes.post.removed).toBe(true);
// Make sure lemmy alpha sees post is removed
let alphaPost = await getPost(alpha, postRes.post.id);
expect(alphaPost.post.removed).toBe(true);
// Undelete
let undeletedPost = await removePost(beta, false, betaPost.post);
expect(undeletedPost.post.removed).toBe(false);
// Make sure lemmy alpha sees post is undeleted
let alphaPost2 = await getPost(alpha, postRes.post.id);
expect(alphaPost2.post.removed).toBe(false);
});
test('Search for a post', async () => {
let search = await searchForBetaCommunity(alpha);
let postRes = await createPost(alpha, search.communities[0].id);