diff --git a/Cargo.lock b/Cargo.lock index caef131ea..22b73f733 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,8 @@ checksum = "8f27d075294830fcab6f66e320dab524bc6d048f4a151698e153205559113772" [[package]] name = "activitypub_federation" -version = "0.6.0-alpha2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4877d467ddf2fac85e9ee33aba6f2560df14125b8bfa864f85ab40e9b87753a9" +version = "0.6.0" +source = "git+https://github.com/LemmyNet/activitypub-federation-rust.git?branch=sign-request#2b5ff07134347c9798a56d1cf4b87054d3bf263f" dependencies = [ "activitystreams-kinds", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index 57bf92431..f92bd7684 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,7 +94,7 @@ lemmy_db_views = { version = "=0.19.6-beta.7", path = "./crates/db_views" } lemmy_db_views_actor = { version = "=0.19.6-beta.7", path = "./crates/db_views_actor" } lemmy_db_views_moderator = { version = "=0.19.6-beta.7", path = "./crates/db_views_moderator" } lemmy_federate = { version = "=0.19.6-beta.7", path = "./crates/federate" } -activitypub_federation = { version = "0.6.0-alpha2", default-features = false, features = [ +activitypub_federation = { git = "https://github.com/LemmyNet/activitypub-federation-rust.git", branch = "sign-request", default-features = false, features = [ "actix-web", ] } diesel = "2.1.6" diff --git a/api_tests/run-federation-test.sh b/api_tests/run-federation-test.sh index 27d2afc26..969a95b3e 100755 --- a/api_tests/run-federation-test.sh +++ b/api_tests/run-federation-test.sh @@ -11,7 +11,7 @@ killall -s1 lemmy_server || true popd pnpm i -pnpm api-test-private-community || true +pnpm api-test || true killall -s1 lemmy_server || true killall -s1 pict-rs || true diff --git a/api_tests/src/private_community.spec.ts b/api_tests/src/private_community.spec.ts index 08a4e5876..65340a1dd 100644 --- a/api_tests/src/private_community.spec.ts +++ b/api_tests/src/private_community.spec.ts @@ -310,7 +310,7 @@ test("Fetch remote content in private community", async () => { p => p?.comment?.comment.id != undefined, ); - // create gamma user and follow community + // create gamma user const gammaCommunityId = ( await resolveCommunity(gamma, community.community_view.community.actor_id) ).community!.community.id; @@ -318,6 +318,12 @@ test("Fetch remote content in private community", async () => { community_id: gammaCommunityId, follow: true, }; + + // cannot fetch post yet + await expect(resolvePost(gamma, post.post_view.post)).rejects.toStrictEqual( + Error("not_found"), + ); + // follow community and approve await gamma.followCommunity(follow_form); await approveFollower(alpha, alphaCommunityId); diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs index 25f6ae0bf..dbcc51258 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -11,6 +11,7 @@ use crate::{ objects::community::ApubCommunity, }; use activitypub_federation::{ + actix_web::signing_actor, config::Data, fetch::object_id::ObjectId, traits::{Collection, Object}, @@ -62,32 +63,52 @@ pub(crate) async fn get_apub_community_followers( info: Path, query: Query, context: Data, + request: HttpRequest, ) -> LemmyResult { let community = Community::read_from_name(&mut context.pool(), &info.community_name, false) .await? .ok_or(LemmyErrorType::NotFound)?; - if community.visibility == CommunityVisibility::Private { - if let Some(is_follower) = &query.is_follower { - // TODO: also check for http sig - let instance_id = is_follower.dereference(&context).await?.instance_id(); - let has_followers = CommunityFollowerView::check_has_followers_from_instance( - community.id, - instance_id, - &mut context.pool(), - ) - .await; - return if has_followers.is_ok() { - Ok(HttpResponse::Ok().finish()) - } else { - Ok(HttpResponse::NotFound().finish()) - }; - } + if let Some(is_follower) = &query.is_follower { + return check_is_follower(community, is_follower, context, request).await; } check_community_fetchable(&community)?; let followers = ApubCommunityFollower::read_local(&community.into(), &context).await?; create_apub_response(&followers) } +/// Checks if a given actor follows the private community. Returns status 200 if true. +async fn check_is_follower( + community: Community, + is_follower: &ObjectId, + context: Data, + request: HttpRequest, +) -> LemmyResult { + if community.visibility != CommunityVisibility::Private { + return Ok(HttpResponse::BadRequest().body("must be a private community")); + } + // also check for http sig so that followers are not exposed publicly + let signing_actor = signing_actor::(&request, None, &context).await?; + CommunityFollowerView::check_has_followers_from_instance( + community.id, + signing_actor.instance_id(), + &mut context.pool(), + ) + .await?; + + let instance_id = is_follower.dereference(&context).await?.instance_id(); + let has_followers = CommunityFollowerView::check_has_followers_from_instance( + community.id, + instance_id, + &mut context.pool(), + ) + .await; + if has_followers.is_ok() { + Ok(HttpResponse::Ok().finish()) + } else { + Ok(HttpResponse::NotFound().finish()) + } +} + /// Returns the community outbox, which is populated by a maximum of 20 posts (but no other /// activities like votes or comments). pub(crate) async fn get_apub_community_outbox( @@ -145,19 +166,13 @@ pub(crate) mod tests { use lemmy_db_schema::{ newtypes::InstanceId, source::{ - community::{ - CommunityFollower, - CommunityFollowerForm, - CommunityFollowerState, - CommunityInsertForm, - }, + community::CommunityInsertForm, instance::Instance, local_site::{LocalSite, LocalSiteInsertForm}, local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitInsertForm}, - person::{Person, PersonInsertForm}, site::{Site, SiteInsertForm}, }, - traits::{Crud, Followable}, + traits::Crud, CommunityVisibility, }; use serde::de::DeserializeOwned; @@ -242,9 +257,13 @@ pub(crate) mod tests { .await?; assert_eq!(200, res.status()); let query = Query(CommunityIsFollowerQuery { is_follower: None }); - let res = - get_apub_community_followers(path.clone().into(), query, context.reset_request_count()) - .await?; + let res = get_apub_community_followers( + path.clone().into(), + query, + context.reset_request_count(), + request.clone(), + ) + .await?; assert_eq!(200, res.status()); let res = get_apub_community_moderators(path.clone().into(), context.reset_request_count()).await?; @@ -282,13 +301,18 @@ pub(crate) mod tests { .await; assert!(res.is_err()); let query = Query(CommunityIsFollowerQuery { is_follower: None }); - let res = - get_apub_community_followers(path.clone().into(), query, context.reset_request_count()).await; + let res = get_apub_community_followers( + path.clone().into(), + query, + context.reset_request_count(), + request.clone(), + ) + .await; assert!(res.is_err()); let res = get_apub_community_moderators(path.clone().into(), context.reset_request_count()).await; assert!(res.is_err()); - let res = get_apub_community_outbox(path.into(), context.reset_request_count(), request).await; + let res = get_apub_community_outbox(path, context.reset_request_count(), request).await; assert!(res.is_err()); //Community::delete(&mut context.pool(), community.id).await?; @@ -317,78 +341,21 @@ pub(crate) mod tests { .await; assert!(res.is_err()); let query = Query(CommunityIsFollowerQuery { is_follower: None }); - let res = - get_apub_community_followers(path.clone().into(), query, context.reset_request_count()).await; + let res = get_apub_community_followers( + path.clone().into(), + query, + context.reset_request_count(), + request.clone(), + ) + .await; assert!(res.is_err()); let res = get_apub_community_moderators(path.clone().into(), context.reset_request_count()).await; assert!(res.is_err()); - let res = get_apub_community_outbox(path.into(), context.reset_request_count(), request).await; + let res = get_apub_community_outbox(path, context.reset_request_count(), request).await; assert!(res.is_err()); Instance::delete(&mut context.pool(), instance.id).await?; Ok(()) } - - #[tokio::test] - #[serial] - async fn test_is_follower() -> LemmyResult<()> { - let context = LemmyContext::init_test_context().await; - let pool = &mut context.pool(); - - // insert local community - let local_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - let community_form = CommunityInsertForm { - visibility: Some(CommunityVisibility::Private), - ..CommunityInsertForm::new( - local_instance.id, - "test_community_4".to_string(), - "nada".to_owned(), - "pubkey".to_string(), - ) - }; - let community = Community::create(pool, &community_form).await?; - - // insert remote user - let remote_instance = Instance::read_or_create(pool, "other_domain.tld".to_string()).await?; - let person_form = - PersonInsertForm::new("name".to_string(), "pubkey".to_string(), remote_instance.id); - let person = Person::create(pool, &person_form).await?; - - // community has no follower from remote instance, returns error - let path: Path = CommunityPath { - community_name: community.name.clone(), - } - .into(); - let query = Query(CommunityIsFollowerQuery { - is_follower: Some(person.actor_id.clone().into()), - }); - let res = get_apub_community_followers( - path.clone().into(), - query.clone().into(), - context.reset_request_count(), - ) - .await; - assert_eq!(404, res?.status()); - - // insert approved follower - let follower_form = CommunityFollowerForm { - state: Some(CommunityFollowerState::Accepted), - ..CommunityFollowerForm::new(community.id, person.id) - }; - CommunityFollower::follow(pool, &follower_form).await?; - - // now returns ok - let res = get_apub_community_followers( - path.clone().into(), - query.into(), - context.reset_request_count(), - ) - .await; - assert_eq!(200, res?.status()); - - Instance::delete(pool, local_instance.id).await?; - Instance::delete(pool, remote_instance.id).await?; - Ok(()) - } } diff --git a/crates/apub/src/http/mod.rs b/crates/apub/src/http/mod.rs index 9dc970240..fc2fbf0d3 100644 --- a/crates/apub/src/http/mod.rs +++ b/crates/apub/src/http/mod.rs @@ -160,12 +160,9 @@ async fn check_community_content_fetchable( followers_url .query_pairs_mut() .append_pair("is_follower", signing_actor.id().as_str()); - context - .client() - .get(followers_url.as_str()) - .send() - .await? - .error_for_status()?; + let req = context.client().get(followers_url.as_str()); + let req = context.sign_request(req, Bytes::new()).await?; + context.client().execute(req).await?.error_for_status()?; Ok(()) } else { Err(LemmyErrorType::NotFound.into())