commit: 4557cd960e4345dd218eb0cf07751ea1545e4a27
parent dd7f699d4a3580a59405fbfc51b96f90f5d84d7d
Author: feld <feld@feld.me>
Date:   Fri, 11 Oct 2024 20:23:46 +0000
Merge branch 'remote-report-policy' into 'develop'
Remote report policy
See merge request pleroma/pleroma!4280
Diffstat:
4 files changed, 279 insertions(+), 0 deletions(-)
diff --git a/changelog.d/remote-report-policy.add b/changelog.d/remote-report-policy.add
@@ -0,0 +1 @@
+Added RemoteReportPolicy from Rebased for handling bogus federated reports
diff --git a/config/config.exs b/config/config.exs
@@ -434,6 +434,11 @@ config :pleroma, :mrf_follow_bot, follower_nickname: nil
 
 config :pleroma, :mrf_inline_quote, template: "<bdi>RT:</bdi> {url}"
 
+config :pleroma, :mrf_remote_report,
+  reject_all: false,
+  reject_anonymous: true,
+  reject_empty_message: true
+
 config :pleroma, :mrf_force_mention,
   mention_parent: true,
   mention_quoted: true
diff --git a/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex b/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex
@@ -0,0 +1,118 @@
+defmodule Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy do
+  @moduledoc "Drop remote reports if they don't contain enough information."
+  @behaviour Pleroma.Web.ActivityPub.MRF.Policy
+
+  alias Pleroma.Config
+
+  @impl true
+  def filter(%{"type" => "Flag"} = object) do
+    with {_, false} <- {:local, local?(object)},
+         {:ok, _} <- maybe_reject_all(object),
+         {:ok, _} <- maybe_reject_anonymous(object),
+         {:ok, _} <- maybe_reject_third_party(object),
+         {:ok, _} <- maybe_reject_empty_message(object) do
+      {:ok, object}
+    else
+      {:local, true} -> {:ok, object}
+      {:reject, message} -> {:reject, message}
+      error -> {:reject, error}
+    end
+  end
+
+  def filter(object), do: {:ok, object}
+
+  defp maybe_reject_all(object) do
+    if Config.get([:mrf_remote_report, :reject_all]) do
+      {:reject, "[RemoteReportPolicy] Remote report"}
+    else
+      {:ok, object}
+    end
+  end
+
+  defp maybe_reject_anonymous(%{"actor" => actor} = object) do
+    with true <- Config.get([:mrf_remote_report, :reject_anonymous]),
+         %URI{path: "/actor"} <- URI.parse(actor) do
+      {:reject, "[RemoteReportPolicy] Anonymous: #{actor}"}
+    else
+      _ -> {:ok, object}
+    end
+  end
+
+  defp maybe_reject_third_party(%{"object" => objects} = object) do
+    {_, to} =
+      case objects do
+        [head | tail] when is_binary(head) -> {tail, head}
+        s when is_binary(s) -> {[], s}
+        _ -> {[], ""}
+      end
+
+    with true <- Config.get([:mrf_remote_report, :reject_third_party]),
+         false <- String.starts_with?(to, Pleroma.Web.Endpoint.url()) do
+      {:reject, "[RemoteReportPolicy] Third-party: #{to}"}
+    else
+      _ -> {:ok, object}
+    end
+  end
+
+  defp maybe_reject_empty_message(%{"content" => content} = object)
+       when is_binary(content) and content != "" do
+    {:ok, object}
+  end
+
+  defp maybe_reject_empty_message(object) do
+    if Config.get([:mrf_remote_report, :reject_empty_message]) do
+      {:reject, ["RemoteReportPolicy] No content"]}
+    else
+      {:ok, object}
+    end
+  end
+
+  defp local?(%{"actor" => actor}) do
+    String.starts_with?(actor, Pleroma.Web.Endpoint.url())
+  end
+
+  @impl true
+  def describe do
+    mrf_remote_report =
+      Config.get(:mrf_remote_report)
+      |> Enum.into(%{})
+
+    {:ok, %{mrf_remote_report: mrf_remote_report}}
+  end
+
+  @impl true
+  def config_description do
+    %{
+      key: :mrf_remote_report,
+      related_policy: "Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy",
+      label: "MRF Remote Report",
+      description: "Drop remote reports if they don't contain enough information.",
+      children: [
+        %{
+          key: :reject_all,
+          type: :boolean,
+          description: "Reject all remote reports? (this option takes precedence)",
+          suggestions: [false]
+        },
+        %{
+          key: :reject_anonymous,
+          type: :boolean,
+          description: "Reject anonymous remote reports?",
+          suggestions: [true]
+        },
+        %{
+          key: :reject_third_party,
+          type: :boolean,
+          description: "Reject reports on users from third-party instances?",
+          suggestions: [true]
+        },
+        %{
+          key: :reject_empty_message,
+          type: :boolean,
+          description: "Reject remote reports with no message?",
+          suggestions: [true]
+        }
+      ]
+    }
+  end
+end
diff --git a/test/pleroma/web/activity_pub/mrf/remote_report_policy_test.exs b/test/pleroma/web/activity_pub/mrf/remote_report_policy_test.exs
@@ -0,0 +1,155 @@
+defmodule Pleroma.Web.ActivityPub.MRF.RemoteReportPolicyTest do
+  use Pleroma.DataCase, async: true
+
+  alias Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy
+
+  setup do
+    clear_config([:mrf_remote_report, :reject_all], false)
+  end
+
+  test "doesn't impact local report" do
+    clear_config([:mrf_remote_report, :reject_anonymous], true)
+    clear_config([:mrf_remote_report, :reject_empty_message], true)
+
+    activity = %{
+      "type" => "Flag",
+      "actor" => "http://localhost:4001/actor",
+      "object" => ["https://mastodon.online/users/Gargron"]
+    }
+
+    assert {:ok, _} = RemoteReportPolicy.filter(activity)
+  end
+
+  test "rejects anonymous report if `reject_anonymous: true`" do
+    clear_config([:mrf_remote_report, :reject_anonymous], true)
+    clear_config([:mrf_remote_report, :reject_empty_message], true)
+
+    activity = %{
+      "type" => "Flag",
+      "actor" => "https://mastodon.social/actor",
+      "object" => ["https://mastodon.online/users/Gargron"]
+    }
+
+    assert {:reject, _} = RemoteReportPolicy.filter(activity)
+  end
+
+  test "preserves anonymous report if `reject_anonymous: false`" do
+    clear_config([:mrf_remote_report, :reject_anonymous], false)
+    clear_config([:mrf_remote_report, :reject_empty_message], false)
+
+    activity = %{
+      "type" => "Flag",
+      "actor" => "https://mastodon.social/actor",
+      "object" => ["https://mastodon.online/users/Gargron"]
+    }
+
+    assert {:ok, _} = RemoteReportPolicy.filter(activity)
+  end
+
+  test "rejects report on third party if `reject_third_party: true`" do
+    clear_config([:mrf_remote_report, :reject_third_party], true)
+    clear_config([:mrf_remote_report, :reject_empty_message], false)
+
+    activity = %{
+      "type" => "Flag",
+      "actor" => "https://mastodon.social/users/Gargron",
+      "object" => ["https://mastodon.online/users/Gargron"]
+    }
+
+    assert {:reject, _} = RemoteReportPolicy.filter(activity)
+  end
+
+  test "preserves report on first party if `reject_third_party: true`" do
+    clear_config([:mrf_remote_report, :reject_third_party], true)
+    clear_config([:mrf_remote_report, :reject_empty_message], false)
+
+    activity = %{
+      "type" => "Flag",
+      "actor" => "https://mastodon.social/users/Gargron",
+      "object" => ["http://localhost:4001/actor"]
+    }
+
+    assert {:ok, _} = RemoteReportPolicy.filter(activity)
+  end
+
+  test "preserves report on third party if `reject_third_party: false`" do
+    clear_config([:mrf_remote_report, :reject_third_party], false)
+    clear_config([:mrf_remote_report, :reject_empty_message], false)
+
+    activity = %{
+      "type" => "Flag",
+      "actor" => "https://mastodon.social/users/Gargron",
+      "object" => ["https://mastodon.online/users/Gargron"]
+    }
+
+    assert {:ok, _} = RemoteReportPolicy.filter(activity)
+  end
+
+  test "rejects empty message report if `reject_empty_message: true`" do
+    clear_config([:mrf_remote_report, :reject_anonymous], false)
+    clear_config([:mrf_remote_report, :reject_empty_message], true)
+
+    activity = %{
+      "type" => "Flag",
+      "actor" => "https://mastodon.social/users/Gargron",
+      "object" => ["https://mastodon.online/users/Gargron"]
+    }
+
+    assert {:reject, _} = RemoteReportPolicy.filter(activity)
+  end
+
+  test "rejects empty message report (\"\") if `reject_empty_message: true`" do
+    clear_config([:mrf_remote_report, :reject_anonymous], false)
+    clear_config([:mrf_remote_report, :reject_empty_message], true)
+
+    activity = %{
+      "type" => "Flag",
+      "actor" => "https://mastodon.social/users/Gargron",
+      "object" => ["https://mastodon.online/users/Gargron"],
+      "content" => ""
+    }
+
+    assert {:reject, _} = RemoteReportPolicy.filter(activity)
+  end
+
+  test "preserves empty message report if `reject_empty_message: false`" do
+    clear_config([:mrf_remote_report, :reject_anonymous], false)
+    clear_config([:mrf_remote_report, :reject_empty_message], false)
+
+    activity = %{
+      "type" => "Flag",
+      "actor" => "https://mastodon.social/users/Gargron",
+      "object" => ["https://mastodon.online/users/Gargron"]
+    }
+
+    assert {:ok, _} = RemoteReportPolicy.filter(activity)
+  end
+
+  test "preserves anonymous, empty message report with all settings disabled" do
+    clear_config([:mrf_remote_report, :reject_anonymous], false)
+    clear_config([:mrf_remote_report, :reject_empty_message], false)
+
+    activity = %{
+      "type" => "Flag",
+      "actor" => "https://mastodon.social/actor",
+      "object" => ["https://mastodon.online/users/Gargron"]
+    }
+
+    assert {:ok, _} = RemoteReportPolicy.filter(activity)
+  end
+
+  test "reject remote report if `reject_all: true`" do
+    clear_config([:mrf_remote_report, :reject_all], true)
+    clear_config([:mrf_remote_report, :reject_anonymous], false)
+    clear_config([:mrf_remote_report, :reject_empty_message], false)
+
+    activity = %{
+      "type" => "Flag",
+      "actor" => "https://mastodon.social/users/Gargron",
+      "content" => "Transphobia",
+      "object" => ["https://mastodon.online/users/Gargron"]
+    }
+
+    assert {:reject, _} = RemoteReportPolicy.filter(activity)
+  end
+end