From bdb4350624516e408a03de6c3bca878430829169 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Wed, 19 Aug 2020 11:28:23 +0200 Subject: [PATCH] Add an authenticated fetch route for members If the member is remote, it redirects to original instance Signed-off-by: Thomas Citharel --- .../activity_stream/converter/member.ex | 2 +- lib/web/cache/activity_pub.ex | 19 ++++++- lib/web/cache/cache.ex | 1 + .../controllers/activity_pub_controller.ex | 48 +++++++++++++++- lib/web/router.ex | 1 + lib/web/views/activity_pub/actor_view.ex | 6 ++ .../activity_pub_controller_test.exs | 56 +++++++++++++++++++ 7 files changed, 130 insertions(+), 3 deletions(-) diff --git a/lib/federation/activity_stream/converter/member.ex b/lib/federation/activity_stream/converter/member.ex index c90c23241..c860fa3fc 100644 --- a/lib/federation/activity_stream/converter/member.ex +++ b/lib/federation/activity_stream/converter/member.ex @@ -29,7 +29,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Member do "id" => member.url, "actor" => member.actor.url, "object" => member.parent.url, - "role" => member.role + "role" => to_string(member.role) } end diff --git a/lib/web/cache/activity_pub.ex b/lib/web/cache/activity_pub.ex index c4401fd32..bb9df1e41 100644 --- a/lib/web/cache/activity_pub.ex +++ b/lib/web/cache/activity_pub.ex @@ -4,7 +4,7 @@ defmodule Mobilizon.Web.Cache.ActivityPub do """ alias Mobilizon.{Actors, Discussions, Events, Posts, Resources, Todos, Tombstone} - alias Mobilizon.Actors.Actor + alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Events.Event alias Mobilizon.Federation.ActivityPub.Relay @@ -174,6 +174,23 @@ defmodule Mobilizon.Web.Cache.ActivityPub do end) end + @doc """ + Gets a member by its UUID, with all associations loaded. + """ + @spec get_member_by_uuid_with_preload(String.t()) :: + {:commit, Todo.t()} | {:ignore, nil} + def get_member_by_uuid_with_preload(uuid) do + Cachex.fetch(@cache, "member_" <> uuid, fn "member_" <> uuid -> + case Actors.get_member(uuid) do + %Member{} = member -> + {:commit, member} + + nil -> + {:ignore, nil} + end + end) + end + @doc """ Gets a relay. """ diff --git a/lib/web/cache/cache.ex b/lib/web/cache/cache.ex index 9b6bce9e0..5afcd4690 100644 --- a/lib/web/cache/cache.ex +++ b/lib/web/cache/cache.ex @@ -24,6 +24,7 @@ defmodule Mobilizon.Web.Cache do defdelegate get_resource_by_uuid_with_preload(uuid), to: ActivityPub defdelegate get_todo_list_by_uuid_with_preload(uuid), to: ActivityPub defdelegate get_todo_by_uuid_with_preload(uuid), to: ActivityPub + defdelegate get_member_by_uuid_with_preload(uuid), to: ActivityPub defdelegate get_post_by_slug_with_preload(slug), to: ActivityPub defdelegate get_discussion_by_slug_with_preload(slug), to: ActivityPub defdelegate get_relay, to: ActivityPub diff --git a/lib/web/controllers/activity_pub_controller.ex b/lib/web/controllers/activity_pub_controller.ex index e4739a4d9..0e35181c4 100644 --- a/lib/web/controllers/activity_pub_controller.ex +++ b/lib/web/controllers/activity_pub_controller.ex @@ -7,7 +7,7 @@ defmodule Mobilizon.Web.ActivityPubController do use Mobilizon.Web, :controller alias Mobilizon.{Actors, Config} - alias Mobilizon.Actors.Actor + alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Federator @@ -66,6 +66,41 @@ defmodule Mobilizon.Web.ActivityPubController do actor_collection(conn, "discussions", args) end + @ok_statuses [:ok, :commit] + @spec member(Plug.Conn.t(), map) :: {:error, :not_found} | Plug.Conn.t() + def member(conn, %{"uuid" => uuid}) do + with {status, %Member{parent: %Actor{} = group, actor: %Actor{domain: nil} = _actor} = member} + when status in @ok_statuses <- + Cache.get_member_by_uuid_with_preload(uuid), + actor <- Map.get(conn.assigns, :actor), + true <- actor_applicant_group_member?(group, actor) do + json( + conn, + ActorView.render("member.json", %{ + member: member, + actor_applicant: actor + }) + ) + else + {status, %Member{actor: %Actor{url: domain}, parent: %Actor{} = group, url: url}} + when status in @ok_statuses and not is_nil(domain) -> + with actor <- Map.get(conn.assigns, :actor), + true <- actor_applicant_group_member?(group, actor) do + redirect(conn, external: url) + else + _ -> + conn + |> put_status(404) + |> json("Not found") + end + + _ -> + conn + |> put_status(404) + |> json("Not found") + end + end + def outbox(conn, args) do actor_collection(conn, "outbox", args) end @@ -153,4 +188,15 @@ defmodule Mobilizon.Web.ActivityPubController do ) end end + + defp actor_applicant_group_member?(%Actor{}, nil), do: false + + defp actor_applicant_group_member?(%Actor{id: group_id}, %Actor{id: actor_applicant_id}), + do: + Actors.get_member(actor_applicant_id, group_id, [ + :member, + :moderator, + :administrator, + :creator + ]) != {:error, :member_not_found} end diff --git a/lib/web/router.ex b/lib/web/router.ex index e23ce1eff..df4b06a54 100644 --- a/lib/web/router.ex +++ b/lib/web/router.ex @@ -105,6 +105,7 @@ defmodule Mobilizon.Web.Router do get("/@:name/following", ActivityPubController, :following) get("/@:name/followers", ActivityPubController, :followers) get("/@:name/members", ActivityPubController, :members) + get("/member/:uuid", ActivityPubController, :member) end scope "/", Mobilizon.Web do diff --git a/lib/web/views/activity_pub/actor_view.ex b/lib/web/views/activity_pub/actor_view.ex index 3599cf392..5e536dd7d 100644 --- a/lib/web/views/activity_pub/actor_view.ex +++ b/lib/web/views/activity_pub/actor_view.ex @@ -23,6 +23,12 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do |> Map.merge(Utils.make_json_ld_header()) end + def render("member.json", %{member: %Member{} = member}) do + member + |> Convertible.model_to_as() + |> Map.merge(Utils.make_json_ld_header()) + end + @doc """ Render an actor collection """ diff --git a/test/web/controllers/activity_pub_controller_test.exs b/test/web/controllers/activity_pub_controller_test.exs index e5dcbf921..c5548a52b 100644 --- a/test/web/controllers/activity_pub_controller_test.exs +++ b/test/web/controllers/activity_pub_controller_test.exs @@ -438,4 +438,60 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do assert result["totalItems"] == 2 end end + + describe "/member/:uuid" do + test "it returns a json representation of the member", %{conn: conn} do + group = insert(:group) + remote_actor_2 = insert(:actor, domain: "remote3.tld") + insert(:member, actor: remote_actor_2, parent: group, role: :member) + + member = + insert(:member, + parent: group, + url: "https://someremote.url/member/here" + ) + + conn = + conn + |> assign(:actor, remote_actor_2) + |> put_req_header("accept", "application/activity+json") + |> get(Routes.activity_pub_url(Endpoint, :member, member.id)) + + assert json_response(conn, 200) == + ActorView.render("member.json", %{member: member}) + end + + test "it redirects for remote comments", %{conn: conn} do + group = insert(:group, domain: "remote1.tld") + remote_actor = insert(:actor, domain: "remote2.tld") + remote_actor_2 = insert(:actor, domain: "remote3.tld") + insert(:member, actor: remote_actor_2, parent: group, role: :member) + + member = + insert(:member, + actor: remote_actor, + parent: group, + url: "https://someremote.url/member/here" + ) + + conn = + conn + |> assign(:actor, remote_actor_2) + |> put_req_header("accept", "application/activity+json") + |> get(Routes.activity_pub_url(Endpoint, :member, member.id)) + + assert redirected_to(conn) == "https://someremote.url/member/here" + end + + test "it returns 404 if the fetch is not authenticated", %{conn: conn} do + member = insert(:member) + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get(Routes.activity_pub_url(Endpoint, :member, member.id)) + + assert json_response(conn, 404) + end + end end