activity_pub_controller: Add authentication to object & activity endpoints
authorHaelwenn (lanodan) Monnier <contact@hacktivis.me>
Thu, 21 Jan 2021 16:45:42 +0000 (17:45 +0100)
committerHaelwenn (lanodan) Monnier <contact@hacktivis.me>
Mon, 8 Feb 2021 19:00:47 +0000 (20:00 +0100)
lib/pleroma/web/activity_pub/activity_pub_controller.ex
lib/pleroma/web/activity_pub/visibility.ex
test/pleroma/web/activity_pub/activity_pub_controller_test.exs
test/pleroma/web/activity_pub/visibility_test.exs

index eb9e119f7d685a108e0cbce2b45f18c8eadbdfe9..9d3dcc7f976122b2bc84dbb2589119bfb332cdb7 100644 (file)
@@ -79,11 +79,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
     end
   end
 
-  def object(conn, _) do
+  def object(%{assigns: assigns} = conn, _) do
     with ap_id <- Endpoint.url() <> conn.request_path,
          %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
-         {_, true} <- {:public?, Visibility.is_public?(object)},
-         {_, false} <- {:local?, Visibility.is_local_public?(object)} do
+         user <- Map.get(assigns, :user, nil),
+         {_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
       conn
       |> assign(:tracking_fun_data, object.id)
       |> set_cache_ttl_for(object)
@@ -91,11 +91,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
       |> put_view(ObjectView)
       |> render("object.json", object: object)
     else
-      {:public?, false} ->
-        {:error, :not_found}
-
-      {:local?, true} ->
-        {:error, :not_found}
+      {:visible?, false} -> {:error, :not_found}
+      nil -> {:error, :not_found}
     end
   end
 
@@ -109,11 +106,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
     conn
   end
 
-  def activity(conn, _params) do
+  def activity(%{assigns: assigns} = conn, _) do
     with ap_id <- Endpoint.url() <> conn.request_path,
          %Activity{} = activity <- Activity.normalize(ap_id),
-         {_, true} <- {:public?, Visibility.is_public?(activity)},
-         {_, false} <- {:local?, Visibility.is_local_public?(activity)} do
+         {_, true} <- {:local?, activity.local},
+         user <- Map.get(assigns, :user, nil),
+         {_, true} <- {:visible?, Visibility.visible_for_user?(activity, user)} do
       conn
       |> maybe_set_tracking_data(activity)
       |> set_cache_ttl_for(activity)
@@ -121,8 +119,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
       |> put_view(ObjectView)
       |> render("object.json", object: activity)
     else
-      {:public?, false} -> {:error, :not_found}
-      {:local?, true} -> {:error, :not_found}
+      {:visible?, false} -> {:error, :not_found}
+      {:local?, false} -> {:error, :not_found}
       nil -> {:error, :not_found}
     end
   end
index 6ef59e93fc62b29a7262762a3a1d58e089bb2957..00234c0b05c7eddf9d0cac3b86f4f8b4e516d73a 100644 (file)
@@ -56,11 +56,10 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
   def is_list?(%{data: %{"listMessage" => _}}), do: true
   def is_list?(_), do: false
 
-  @spec visible_for_user?(Activity.t() | nil, User.t() | nil) :: boolean()
+  @spec visible_for_user?(Object.t() | Activity.t() | nil, User.t() | nil) :: boolean()
   def visible_for_user?(%Activity{actor: ap_id}, %User{ap_id: ap_id}), do: true
-
+  def visible_for_user?(%Object{data: %{"actor" => ap_id}}, %User{ap_id: ap_id}), do: true
   def visible_for_user?(nil, _), do: false
-
   def visible_for_user?(%Activity{data: %{"listMessage" => _}}, nil), do: false
 
   def visible_for_user?(
@@ -73,16 +72,18 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
       |> Pleroma.List.member?(user)
   end
 
-  def visible_for_user?(%Activity{} = activity, nil) do
-    if restrict_unauthenticated_access?(activity),
+  def visible_for_user?(%{__struct__: module} = message, nil)
+      when module in [Activity, Object] do
+    if restrict_unauthenticated_access?(message),
       do: false,
-      else: is_public?(activity)
+      else: is_public?(message) and not is_local_public?(message)
   end
 
-  def visible_for_user?(%Activity{} = activity, user) do
+  def visible_for_user?(%{__struct__: module} = message, user)
+      when module in [Activity, Object] do
     x = [user.ap_id | User.following(user)]
-    y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || [])
-    is_public?(activity) || Enum.any?(x, &(&1 in y))
+    y = [message.data["actor"]] ++ message.data["to"] ++ (message.data["cc"] || [])
+    is_public?(message) || Enum.any?(x, &(&1 in y))
   end
 
   def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
index 91a3109bbb948f3514ed629d6ad5c48047348b9a..5e53b8afcf8913cb7179f185f78008a0775a7e46 100644 (file)
@@ -229,6 +229,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
       assert json_response(conn, 404)
     end
 
+    test "returns local-only objects when authenticated", %{conn: conn} do
+      user = insert(:user)
+      {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
+
+      assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
+
+      object = Object.normalize(post, fetch: false)
+      uuid = String.split(object.data["id"], "/") |> List.last()
+
+      assert response =
+               conn
+               |> assign(:user, user)
+               |> put_req_header("accept", "application/activity+json")
+               |> get("/objects/#{uuid}")
+
+      assert json_response(response, 200) == ObjectView.render("object.json", %{object: object})
+    end
+
     test "it returns a json representation of the object with accept application/json", %{
       conn: conn
     } do
@@ -285,6 +303,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
       assert json_response(conn, 404)
     end
 
+    test "returns visible non-public messages when authenticated", %{conn: conn} do
+      note = insert(:direct_note)
+      uuid = String.split(note.data["id"], "/") |> List.last()
+      user = User.get_by_ap_id(note.data["actor"])
+      marisa = insert(:user)
+
+      assert conn
+             |> assign(:user, marisa)
+             |> put_req_header("accept", "application/activity+json")
+             |> get("/objects/#{uuid}")
+             |> json_response(404)
+
+      assert response =
+               conn
+               |> assign(:user, user)
+               |> put_req_header("accept", "application/activity+json")
+               |> get("/objects/#{uuid}")
+               |> json_response(200)
+
+      assert response == ObjectView.render("object.json", %{object: note})
+    end
+
     test "it returns 404 for tombstone objects", %{conn: conn} do
       tombstone = insert(:tombstone)
       uuid = String.split(tombstone.data["id"], "/") |> List.last()
@@ -358,6 +398,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
       assert json_response(conn, 404)
     end
 
+    test "returns local-only activities when authenticated", %{conn: conn} do
+      user = insert(:user)
+      {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
+
+      assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
+
+      uuid = String.split(post.data["id"], "/") |> List.last()
+
+      assert response =
+               conn
+               |> assign(:user, user)
+               |> put_req_header("accept", "application/activity+json")
+               |> get("/activities/#{uuid}")
+
+      assert json_response(response, 200) == ObjectView.render("object.json", %{object: post})
+    end
+
     test "it returns a json representation of the activity", %{conn: conn} do
       activity = insert(:note_activity)
       uuid = String.split(activity.data["id"], "/") |> List.last()
@@ -382,6 +439,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
       assert json_response(conn, 404)
     end
 
+    test "returns visible non-public messages when authenticated", %{conn: conn} do
+      note = insert(:direct_note_activity)
+      uuid = String.split(note.data["id"], "/") |> List.last()
+      user = User.get_by_ap_id(note.data["actor"])
+      marisa = insert(:user)
+
+      assert conn
+             |> assign(:user, marisa)
+             |> put_req_header("accept", "application/activity+json")
+             |> get("/activities/#{uuid}")
+             |> json_response(404)
+
+      assert response =
+               conn
+               |> assign(:user, user)
+               |> put_req_header("accept", "application/activity+json")
+               |> get("/activities/#{uuid}")
+               |> json_response(200)
+
+      assert response == ObjectView.render("object.json", %{object: note})
+    end
+
     test "it caches a response", %{conn: conn} do
       activity = insert(:note_activity)
       uuid = String.split(activity.data["id"], "/") |> List.last()
index d8544279a3fd4ab18ec2e3abd49cdf9c9d4a6b26..23485225d0b60404c117778518ad237afc8c7ec1 100644 (file)
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
   use Pleroma.DataCase, async: true
 
   alias Pleroma.Activity
+  alias Pleroma.Object
   alias Pleroma.Web.ActivityPub.Visibility
   alias Pleroma.Web.CommonAPI
   import Pleroma.Factory
@@ -107,7 +108,7 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
     assert Visibility.is_list?(list)
   end
 
-  test "visible_for_user?", %{
+  test "visible_for_user? Activity", %{
     public: public,
     private: private,
     direct: direct,
@@ -149,10 +150,76 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
     refute Visibility.visible_for_user?(private, unrelated)
     refute Visibility.visible_for_user?(direct, unrelated)
 
+    # Public and unlisted visible for unauthenticated
+
+    assert Visibility.visible_for_user?(public, nil)
+    assert Visibility.visible_for_user?(unlisted, nil)
+    refute Visibility.visible_for_user?(private, nil)
+    refute Visibility.visible_for_user?(direct, nil)
+
     # Visible for a list member
     assert Visibility.visible_for_user?(list, unrelated)
   end
 
+  test "visible_for_user? Object", %{
+    public: public,
+    private: private,
+    direct: direct,
+    unlisted: unlisted,
+    user: user,
+    mentioned: mentioned,
+    following: following,
+    unrelated: unrelated,
+    list: list
+  } do
+    public = Object.normalize(public)
+    private = Object.normalize(private)
+    unlisted = Object.normalize(unlisted)
+    direct = Object.normalize(direct)
+    list = Object.normalize(list)
+
+    # All visible to author
+
+    assert Visibility.visible_for_user?(public, user)
+    assert Visibility.visible_for_user?(private, user)
+    assert Visibility.visible_for_user?(unlisted, user)
+    assert Visibility.visible_for_user?(direct, user)
+    assert Visibility.visible_for_user?(list, user)
+
+    # All visible to a mentioned user
+
+    assert Visibility.visible_for_user?(public, mentioned)
+    assert Visibility.visible_for_user?(private, mentioned)
+    assert Visibility.visible_for_user?(unlisted, mentioned)
+    assert Visibility.visible_for_user?(direct, mentioned)
+    assert Visibility.visible_for_user?(list, mentioned)
+
+    # DM not visible for just follower
+
+    assert Visibility.visible_for_user?(public, following)
+    assert Visibility.visible_for_user?(private, following)
+    assert Visibility.visible_for_user?(unlisted, following)
+    refute Visibility.visible_for_user?(direct, following)
+    refute Visibility.visible_for_user?(list, following)
+
+    # Public and unlisted visible for unrelated user
+
+    assert Visibility.visible_for_user?(public, unrelated)
+    assert Visibility.visible_for_user?(unlisted, unrelated)
+    refute Visibility.visible_for_user?(private, unrelated)
+    refute Visibility.visible_for_user?(direct, unrelated)
+
+    # Public and unlisted visible for unauthenticated
+
+    assert Visibility.visible_for_user?(public, nil)
+    assert Visibility.visible_for_user?(unlisted, nil)
+    refute Visibility.visible_for_user?(private, nil)
+    refute Visibility.visible_for_user?(direct, nil)
+
+    # Visible for a list member
+    # assert Visibility.visible_for_user?(list, unrelated)
+  end
+
   test "doesn't die when the user doesn't exist",
        %{
          direct: direct,