From bdb4350624516e408a03de6c3bca878430829169 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Wed, 19 Aug 2020 11:28:23 +0200 Subject: [PATCH 1/2] 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 From 4782221ef43f6b38d96e167ee845cb8d7c95e0e1 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Thu, 20 Aug 2020 10:54:58 +0200 Subject: [PATCH 2/2] Allow to update a member role Signed-off-by: Thomas Citharel --- js/src/graphql/member.ts | 9 ++ js/src/i18n/en_US.json | 4 +- js/src/i18n/fr_FR.json | 4 +- js/src/views/Group/GroupMembers.vue | 44 +++++- lib/federation/activity_pub/activity_pub.ex | 9 +- lib/federation/activity_pub/fetcher.ex | 4 - lib/federation/activity_pub/preloader.ex | 4 +- lib/federation/activity_pub/transmogrifier.ex | 21 +++ lib/federation/activity_pub/types/entity.ex | 8 +- lib/federation/activity_pub/types/members.ex | 54 +++++++ lib/graphql/resolvers/member.ex | 26 ++- lib/graphql/schema/actors/member.ex | 7 + test/graphql/resolvers/member_test.exs | 149 ++++++++++++++++++ 13 files changed, 321 insertions(+), 22 deletions(-) create mode 100644 lib/federation/activity_pub/types/members.ex diff --git a/js/src/graphql/member.ts b/js/src/graphql/member.ts index fe2e704bf..59e5cf23c 100644 --- a/js/src/graphql/member.ts +++ b/js/src/graphql/member.ts @@ -81,6 +81,15 @@ export const GROUP_MEMBERS = gql` } `; +export const UPDATE_MEMBER = gql` + mutation UpdateMember($memberId: ID!, $role: MemberRoleEnum!) { + updateMember(memberId: $memberId, role: $role) { + id + role + } + } +`; + export const REMOVE_MEMBER = gql` mutation RemoveMember($groupId: ID!, $memberId: ID!) { removeMember(groupId: $groupId, memberId: $memberId) { diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index 2eca592ef..9da515b2e 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -764,5 +764,7 @@ "Update": "Update", "Search…": "Search…", "Edited {ago}": "Edited {ago}", - "[This comment has been deleted by it's author]": "[This comment has been deleted by it's author]" + "[This comment has been deleted by it's author]": "[This comment has been deleted by it's author]", + "Promote": "Promote", + "Demote": "Demote" } diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index e8442adf8..9c2a07a26 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -765,5 +765,7 @@ "Update": "Éditer", "Search…": "Rechercher…", "Edited {ago}": "Édité {ago}", - "[This comment has been deleted by it's author]": "[Ce commentaire a été supprimé par son auteur]" + "[This comment has been deleted by it's author]": "[Ce commentaire a été supprimé par son auteur]", + "Promote": "Promouvoir", + "Demote": "Rétrograder" } diff --git a/js/src/views/Group/GroupMembers.vue b/js/src/views/Group/GroupMembers.vue index 9996a9444..e6fa34c04 100644 --- a/js/src/views/Group/GroupMembers.vue +++ b/js/src/views/Group/GroupMembers.vue @@ -134,12 +134,24 @@ - {{ $t("Remove") }} +
+ {{ $t("Promote") }} + {{ $t("Demote") }} + {{ $t("Remove") }} +