extend reject MRF to check if originating instance is blocked
authorFloatingGhost <hannah@coffee-and-dreams.uk>
Fri, 9 Dec 2022 19:57:29 +0000 (19:57 +0000)
committerFloatingGhost <hannah@coffee-and-dreams.uk>
Fri, 9 Dec 2022 19:57:29 +0000 (19:57 +0000)
CHANGELOG.md
config/config.exs
lib/pleroma/config/deprecation_warnings.ex
lib/pleroma/web/activity_pub/mrf/simple_policy.ex
test/pleroma/web/activity_pub/mrf/simple_policy_test.exs

index 07ed6653a6761b9894ee6ab928ce4ddbe41b3087..017ec6a8cabf4b71a0fad2234d0e68d4d40a84a2 100644 (file)
@@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Regular task to prune local transient activities
 - Task to manually run the transient prune job (pleroma.database prune\_task)
 - Ability to follow hashtags
+- Option to extend `reject` in MRF-Simple to apply to entire threads, where the originating instance is rejected
 
 ## Changed
 - MastoAPI: Accept BooleanLike input on `/api/v1/accounts/:id/follow` (fixes follows with mastodon.py)
index 8e01044004f5eba93d330c32fe74bb53f3f07cf9..4d6150634fc6033176e0914ef6db424eeab0c719 100644 (file)
@@ -391,7 +391,8 @@ config :pleroma, :mrf_simple,
   accept: [],
   avatar_removal: [],
   banner_removal: [],
-  reject_deletes: []
+  reject_deletes: [],
+  handle_threads: true
 
 config :pleroma, :mrf_keyword,
   reject: [],
index 8a336c35a3d5ace64455f7926e5a2ccf68603b1c..73ef41145182440e88bd12e25873994bb09d072d 100644 (file)
@@ -25,7 +25,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
   def check_simple_policy_tuples do
     has_strings =
       Config.get([:mrf_simple])
-      |> Enum.any?(fn {_, v} -> Enum.any?(v, &is_binary/1) end)
+      |> Enum.any?(fn {_, v} -> is_list(v) and Enum.any?(v, &is_binary/1) end)
 
     if has_strings do
       Logger.warn("""
@@ -66,6 +66,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
 
       new_config =
         Config.get([:mrf_simple])
+        |> Enum.filter(fn {k, v} -> not is_atom(v) end)
         |> Enum.map(fn {k, v} ->
           {k,
            Enum.map(v, fn
index a59212db461ab61fa9c22dbfc12cc159d17eab82..f7eb0f159326687e76b1f97c3690ad4636394caa 100644 (file)
@@ -13,20 +13,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
 
   require Pleroma.Constants
 
-  defp check_accept(%{host: actor_host} = _actor_info, object) do
+  defp check_accept(%{host: actor_host} = _actor_info) do
     accepts =
       instance_list(:accept)
       |> MRF.subdomains_regex()
 
     cond do
-      accepts == [] -> {:ok, object}
-      actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
-      MRF.subdomain_match?(accepts, actor_host) -> {:ok, object}
+      accepts == [] -> {:ok, nil}
+      actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, nil}
+      MRF.subdomain_match?(accepts, actor_host) -> {:ok, nil}
       true -> {:reject, "[SimplePolicy] host not in accept list"}
     end
   end
 
-  defp check_reject(%{host: actor_host} = _actor_info, object) do
+  defp check_reject(%{host: actor_host} = _actor_info) do
     rejects =
       instance_list(:reject)
       |> MRF.subdomains_regex()
@@ -34,7 +34,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
     if MRF.subdomain_match?(rejects, actor_host) do
       {:reject, "[SimplePolicy] host in reject list"}
     else
-      {:ok, object}
+      {:ok, nil}
     end
   end
 
@@ -178,6 +178,55 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
 
   defp check_banner_removal(_actor_info, object), do: {:ok, object}
 
+  defp extract_context_uri(%{"conversation" => "tag:" <> rest}) do
+    rest
+    |> String.split(",", parts: 2, trim: true)
+    |> hd()
+    |> case do
+      nil -> nil
+      hostname -> URI.parse("//" <> hostname)
+    end
+  end
+
+  defp extract_context_uri(%{"context" => "http" <> _ = context}), do: URI.parse(context)
+
+  defp extract_context_uri(_), do: nil
+
+  defp check_context(activity) do
+    uri = extract_context_uri(activity)
+
+    with {:uri, true} <- {:uri, Kernel.match?(%URI{}, uri)},
+         {:ok, _} <- check_accept(uri),
+         {:ok, _} <- check_reject(uri) do
+      {:ok, activity}
+    else
+      # Can't check.
+      {:uri, false} -> {:ok, activity}
+      {:reject, nil} -> {:reject, "[SimplePolicy]"}
+      {:reject, _} = e -> e
+      _ -> {:reject, "[SimplePolicy]"}
+    end
+  end
+
+  defp check_reply_to(%{"object" => %{"inReplyTo" => in_reply_to}} = activity) do
+    with {:ok, _} <- filter(in_reply_to) do
+      {:ok, activity}
+    end
+  end
+
+  defp check_reply_to(activity), do: {:ok, activity}
+
+  defp maybe_check_thread(activity) do
+    if Config.get([:mrf_simple, :handle_threads], true) do
+      with {:ok, _} <- check_context(activity),
+           {:ok, _} <- check_reply_to(activity) do
+        {:ok, activity}
+      end
+    else
+      {:ok, activity}
+    end
+  end
+
   defp check_object(%{"object" => object} = activity) do
     with {:ok, _object} <- filter(object) do
       {:ok, activity}
@@ -210,13 +259,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
   def filter(%{"actor" => actor} = object) do
     actor_info = URI.parse(actor)
 
-    with {:ok, object} <- check_accept(actor_info, object),
-         {:ok, object} <- check_reject(actor_info, object),
+    with {:ok, _} <- check_accept(actor_info),
+         {:ok, _} <- check_reject(actor_info),
          {:ok, object} <- check_media_removal(actor_info, object),
          {:ok, object} <- check_media_nsfw(actor_info, object),
          {:ok, object} <- check_ftl_removal(actor_info, object),
          {:ok, object} <- check_followers_only(actor_info, object),
          {:ok, object} <- check_report_removal(actor_info, object),
+         {:ok, object} <- maybe_check_thread(object),
          {:ok, object} <- check_object(object) do
       {:ok, object}
     else
@@ -230,8 +280,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
       when obj_type in ["Application", "Group", "Organization", "Person", "Service"] do
     actor_info = URI.parse(actor)
 
-    with {:ok, object} <- check_accept(actor_info, object),
-         {:ok, object} <- check_reject(actor_info, object),
+    with {:ok, _} <- check_accept(actor_info),
+         {:ok, _} <- check_reject(actor_info),
          {:ok, object} <- check_avatar_removal(actor_info, object),
          {:ok, object} <- check_banner_removal(actor_info, object) do
       {:ok, object}
@@ -242,11 +292,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
     end
   end
 
+  def filter(%{"id" => id} = object) do
+    with {:ok, _} <- filter(id) do
+      {:ok, object}
+    end
+  end
+
   def filter(object) when is_binary(object) do
     uri = URI.parse(object)
 
-    with {:ok, object} <- check_accept(uri, object),
-         {:ok, object} <- check_reject(uri, object) do
+    with {:ok, _} <- check_accept(uri),
+         {:ok, _} <- check_reject(uri) do
       {:ok, object}
     else
       {:reject, nil} -> {:reject, "[SimplePolicy]"}
@@ -288,6 +344,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
 
     mrf_simple_excluded =
       Config.get(:mrf_simple)
+      |> Enum.filter(fn {_, v} -> is_list(v) end)
       |> Enum.map(fn {rule, instances} ->
         {rule, Enum.reject(instances, fn {host, _} -> host in exclusions end)}
       end)
@@ -332,66 +389,78 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
       label: "MRF Simple",
       description: "Simple ingress policies",
       children:
-        [
-          %{
-            key: :media_removal,
-            description:
-              "List of instances to strip media attachments from and the reason for doing so"
-          },
-          %{
-            key: :media_nsfw,
-            label: "Media NSFW",
-            description:
-              "List of instances to tag all media as NSFW (sensitive) from and the reason for doing so"
-          },
-          %{
-            key: :federated_timeline_removal,
-            description:
-              "List of instances to remove from the Federated (aka The Whole Known Network) Timeline and the reason for doing so"
-          },
-          %{
-            key: :reject,
-            description:
-              "List of instances to reject activities from (except deletes) and the reason for doing so"
-          },
-          %{
-            key: :accept,
-            description:
-              "List of instances to only accept activities from (except deletes) and the reason for doing so"
-          },
-          %{
-            key: :followers_only,
-            description:
-              "Force posts from the given instances to be visible by followers only and the reason for doing so"
-          },
-          %{
-            key: :report_removal,
-            description: "List of instances to reject reports from and the reason for doing so"
-          },
-          %{
-            key: :avatar_removal,
-            description: "List of instances to strip avatars from and the reason for doing so"
-          },
-          %{
-            key: :banner_removal,
-            description: "List of instances to strip banners from and the reason for doing so"
-          },
-          %{
-            key: :reject_deletes,
-            description: "List of instances to reject deletions from and the reason for doing so"
-          }
-        ]
-        |> Enum.map(fn setting ->
-          Map.merge(
-            setting,
+        ([
+           %{
+             key: :media_removal,
+             description:
+               "List of instances to strip media attachments from and the reason for doing so"
+           },
+           %{
+             key: :media_nsfw,
+             label: "Media NSFW",
+             description:
+               "List of instances to tag all media as NSFW (sensitive) from and the reason for doing so"
+           },
+           %{
+             key: :federated_timeline_removal,
+             description:
+               "List of instances to remove from the Federated (aka The Whole Known Network) Timeline and the reason for doing so"
+           },
+           %{
+             key: :reject,
+             description:
+               "List of instances to reject activities from (except deletes) and the reason for doing so"
+           },
+           %{
+             key: :accept,
+             description:
+               "List of instances to only accept activities from (except deletes) and the reason for doing so"
+           },
+           %{
+             key: :followers_only,
+             description:
+               "Force posts from the given instances to be visible by followers only and the reason for doing so"
+           },
+           %{
+             key: :report_removal,
+             description: "List of instances to reject reports from and the reason for doing so"
+           },
+           %{
+             key: :avatar_removal,
+             description: "List of instances to strip avatars from and the reason for doing so"
+           },
+           %{
+             key: :banner_removal,
+             description: "List of instances to strip banners from and the reason for doing so"
+           },
+           %{
+             key: :reject_deletes,
+             description: "List of instances to reject deletions from and the reason for doing so"
+           }
+         ]
+         |> Enum.map(fn setting ->
+           Map.merge(
+             setting,
+             %{
+               type: {:list, :tuple},
+               key_placeholder: "instance",
+               value_placeholder: "reason",
+               suggestions: [
+                 {"example.com", "Some reason"},
+                 {"*.example.com", "Another reason"}
+               ]
+             }
+           )
+         end)) ++
+          [
             %{
-              type: {:list, :tuple},
-              key_placeholder: "instance",
-              value_placeholder: "reason",
-              suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}]
+              key: :handle_threads,
+              label: "Apply to entire threads",
+              type: :boolean,
+              description:
+                "Enable to filter replies to threads based from their originating instance, using the reject and accept rules"
             }
-          )
-        end)
+          ]
     }
   end
 end
index 036573171e719af7cdbaa50931592281e99c382e..875cf8f43232db382c8a40c15851c98f9c3f6fc7 100644 (file)
@@ -356,6 +356,86 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
 
       assert {:reject, _} = SimplePolicy.filter(announce)
     end
+
+    test "accept by matching context URI if :handle_threads is disabled" do
+      clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
+      clear_config([:mrf_simple, :handle_threads], false)
+
+      remote_message =
+        build_remote_message()
+        |> Map.put("context", "https://blocked.tld/contexts/abc")
+
+      assert {:ok, _} = SimplePolicy.filter(remote_message)
+    end
+
+    test "accept by matching conversation field if :handle_threads is disabled" do
+      clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
+      clear_config([:mrf_simple, :handle_threads], false)
+
+      remote_message =
+        build_remote_message()
+        |> Map.put(
+          "conversation",
+          "tag:blocked.tld,1997-06-25:objectId=12345:objectType=Conversation"
+        )
+
+      assert {:ok, _} = SimplePolicy.filter(remote_message)
+    end
+
+    test "accept by matching reply ID if :handle_threads is disabled" do
+      clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
+      clear_config([:mrf_simple, :handle_threads], false)
+
+      remote_message =
+        build_remote_message()
+        |> Map.put("type", "Create")
+        |> Map.put("object", %{
+          "type" => "Note",
+          "inReplyTo" => "https://blocked.tld/objects/1"
+        })
+
+      assert {:ok, _} = SimplePolicy.filter(remote_message)
+    end
+
+    test "reject by matching context URI if :handle_threads is enabled" do
+      clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
+      clear_config([:mrf_simple, :handle_threads], true)
+
+      remote_message =
+        build_remote_message()
+        |> Map.put("context", "https://blocked.tld/contexts/abc")
+
+      assert {:reject, _} = SimplePolicy.filter(remote_message)
+    end
+
+    test "reject by matching conversation field if :handle_threads is enabled" do
+      clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
+      clear_config([:mrf_simple, :handle_threads], true)
+
+      remote_message =
+        build_remote_message()
+        |> Map.put(
+          "conversation",
+          "tag:blocked.tld,1997-06-25:objectId=12345:objectType=Conversation"
+        )
+
+      assert {:reject, _} = SimplePolicy.filter(remote_message)
+    end
+
+    test "reject by matching reply ID if :handle_threads is enabled" do
+      clear_config([:mrf_simple, :reject], [{"blocked.tld", ""}])
+      clear_config([:mrf_simple, :handle_threads], true)
+
+      remote_message =
+        build_remote_message()
+        |> Map.put("type", "Create")
+        |> Map.put("object", %{
+          "type" => "Note",
+          "inReplyTo" => "https://blocked.tld/objects/1"
+        })
+
+      assert {:reject, _} = SimplePolicy.filter(remote_message)
+    end
   end
 
   describe "when :followers_only" do