Add an authenticated fetch route for members

If the member is remote, it redirects to original instance

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2020-08-19 11:28:23 +02:00
parent b4c624de23
commit bdb4350624
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
7 changed files with 130 additions and 3 deletions

View file

@ -29,7 +29,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Member do
"id" => member.url, "id" => member.url,
"actor" => member.actor.url, "actor" => member.actor.url,
"object" => member.parent.url, "object" => member.parent.url,
"role" => member.role "role" => to_string(member.role)
} }
end end

View file

@ -4,7 +4,7 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
""" """
alias Mobilizon.{Actors, Discussions, Events, Posts, Resources, Todos, Tombstone} 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.Discussions.{Comment, Discussion}
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub.Relay alias Mobilizon.Federation.ActivityPub.Relay
@ -174,6 +174,23 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
end) end)
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 """ @doc """
Gets a relay. Gets a relay.
""" """

View file

@ -24,6 +24,7 @@ defmodule Mobilizon.Web.Cache do
defdelegate get_resource_by_uuid_with_preload(uuid), to: ActivityPub defdelegate get_resource_by_uuid_with_preload(uuid), to: ActivityPub
defdelegate get_todo_list_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_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_post_by_slug_with_preload(slug), to: ActivityPub
defdelegate get_discussion_by_slug_with_preload(slug), to: ActivityPub defdelegate get_discussion_by_slug_with_preload(slug), to: ActivityPub
defdelegate get_relay, to: ActivityPub defdelegate get_relay, to: ActivityPub

View file

@ -7,7 +7,7 @@ defmodule Mobilizon.Web.ActivityPubController do
use Mobilizon.Web, :controller use Mobilizon.Web, :controller
alias Mobilizon.{Actors, Config} alias Mobilizon.{Actors, Config}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Federator alias Mobilizon.Federation.ActivityPub.Federator
@ -66,6 +66,41 @@ defmodule Mobilizon.Web.ActivityPubController do
actor_collection(conn, "discussions", args) actor_collection(conn, "discussions", args)
end 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 def outbox(conn, args) do
actor_collection(conn, "outbox", args) actor_collection(conn, "outbox", args)
end end
@ -153,4 +188,15 @@ defmodule Mobilizon.Web.ActivityPubController do
) )
end end
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 end

View file

@ -105,6 +105,7 @@ defmodule Mobilizon.Web.Router do
get("/@:name/following", ActivityPubController, :following) get("/@:name/following", ActivityPubController, :following)
get("/@:name/followers", ActivityPubController, :followers) get("/@:name/followers", ActivityPubController, :followers)
get("/@:name/members", ActivityPubController, :members) get("/@:name/members", ActivityPubController, :members)
get("/member/:uuid", ActivityPubController, :member)
end end
scope "/", Mobilizon.Web do scope "/", Mobilizon.Web do

View file

@ -23,6 +23,12 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
end end
def render("member.json", %{member: %Member{} = member}) do
member
|> Convertible.model_to_as()
|> Map.merge(Utils.make_json_ld_header())
end
@doc """ @doc """
Render an actor collection Render an actor collection
""" """

View file

@ -438,4 +438,60 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do
assert result["totalItems"] == 2 assert result["totalItems"] == 2
end end
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 end