extend reject MRF to check if originating instance is blocked

This commit is contained in:
FloatingGhost 2022-12-09 19:57:29 +00:00
parent d5828f1c5e
commit 6f83ae27aa
5 changed files with 224 additions and 72 deletions

View 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 - Regular task to prune local transient activities
- Task to manually run the transient prune job (pleroma.database prune\_task) - Task to manually run the transient prune job (pleroma.database prune\_task)
- Ability to follow hashtags - Ability to follow hashtags
- Option to extend `reject` in MRF-Simple to apply to entire threads, where the originating instance is rejected
## Changed ## Changed
- MastoAPI: Accept BooleanLike input on `/api/v1/accounts/:id/follow` (fixes follows with mastodon.py) - MastoAPI: Accept BooleanLike input on `/api/v1/accounts/:id/follow` (fixes follows with mastodon.py)

View file

@ -391,7 +391,8 @@
accept: [], accept: [],
avatar_removal: [], avatar_removal: [],
banner_removal: [], banner_removal: [],
reject_deletes: [] reject_deletes: [],
handle_threads: true
config :pleroma, :mrf_keyword, config :pleroma, :mrf_keyword,
reject: [], reject: [],

View file

@ -25,7 +25,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
def check_simple_policy_tuples do def check_simple_policy_tuples do
has_strings = has_strings =
Config.get([:mrf_simple]) 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 if has_strings do
Logger.warn(""" Logger.warn("""
@ -66,6 +66,7 @@ def check_simple_policy_tuples do
new_config = new_config =
Config.get([:mrf_simple]) Config.get([:mrf_simple])
|> Enum.filter(fn {k, v} -> not is_atom(v) end)
|> Enum.map(fn {k, v} -> |> Enum.map(fn {k, v} ->
{k, {k,
Enum.map(v, fn Enum.map(v, fn

View file

@ -13,20 +13,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
require Pleroma.Constants require Pleroma.Constants
defp check_accept(%{host: actor_host} = _actor_info, object) do defp check_accept(%{host: actor_host} = _actor_info) do
accepts = accepts =
instance_list(:accept) instance_list(:accept)
|> MRF.subdomains_regex() |> MRF.subdomains_regex()
cond do cond do
accepts == [] -> {:ok, object} accepts == [] -> {:ok, nil}
actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object} actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, nil}
MRF.subdomain_match?(accepts, actor_host) -> {:ok, object} MRF.subdomain_match?(accepts, actor_host) -> {:ok, nil}
true -> {:reject, "[SimplePolicy] host not in accept list"} true -> {:reject, "[SimplePolicy] host not in accept list"}
end end
end end
defp check_reject(%{host: actor_host} = _actor_info, object) do defp check_reject(%{host: actor_host} = _actor_info) do
rejects = rejects =
instance_list(:reject) instance_list(:reject)
|> MRF.subdomains_regex() |> MRF.subdomains_regex()
@ -34,7 +34,7 @@ defp check_reject(%{host: actor_host} = _actor_info, object) do
if MRF.subdomain_match?(rejects, actor_host) do if MRF.subdomain_match?(rejects, actor_host) do
{:reject, "[SimplePolicy] host in reject list"} {:reject, "[SimplePolicy] host in reject list"}
else else
{:ok, object} {:ok, nil}
end end
end end
@ -178,6 +178,55 @@ defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image
defp check_banner_removal(_actor_info, object), do: {:ok, object} 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 defp check_object(%{"object" => object} = activity) do
with {:ok, _object} <- filter(object) do with {:ok, _object} <- filter(object) do
{:ok, activity} {:ok, activity}
@ -210,13 +259,14 @@ def filter(%{"type" => "Delete", "actor" => actor} = object) do
def filter(%{"actor" => actor} = object) do def filter(%{"actor" => actor} = object) do
actor_info = URI.parse(actor) actor_info = URI.parse(actor)
with {:ok, object} <- check_accept(actor_info, object), with {:ok, _} <- check_accept(actor_info),
{:ok, object} <- check_reject(actor_info, object), {:ok, _} <- check_reject(actor_info),
{:ok, object} <- check_media_removal(actor_info, object), {:ok, object} <- check_media_removal(actor_info, object),
{:ok, object} <- check_media_nsfw(actor_info, object), {:ok, object} <- check_media_nsfw(actor_info, object),
{:ok, object} <- check_ftl_removal(actor_info, object), {:ok, object} <- check_ftl_removal(actor_info, object),
{:ok, object} <- check_followers_only(actor_info, object), {:ok, object} <- check_followers_only(actor_info, object),
{:ok, object} <- check_report_removal(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} <- check_object(object) do
{:ok, object} {:ok, object}
else else
@ -230,8 +280,8 @@ def filter(%{"id" => actor, "type" => obj_type} = object)
when obj_type in ["Application", "Group", "Organization", "Person", "Service"] do when obj_type in ["Application", "Group", "Organization", "Person", "Service"] do
actor_info = URI.parse(actor) actor_info = URI.parse(actor)
with {:ok, object} <- check_accept(actor_info, object), with {:ok, _} <- check_accept(actor_info),
{:ok, object} <- check_reject(actor_info, object), {:ok, _} <- check_reject(actor_info),
{:ok, object} <- check_avatar_removal(actor_info, object), {:ok, object} <- check_avatar_removal(actor_info, object),
{:ok, object} <- check_banner_removal(actor_info, object) do {:ok, object} <- check_banner_removal(actor_info, object) do
{:ok, object} {:ok, object}
@ -242,11 +292,17 @@ def filter(%{"id" => actor, "type" => obj_type} = object)
end end
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 def filter(object) when is_binary(object) do
uri = URI.parse(object) uri = URI.parse(object)
with {:ok, object} <- check_accept(uri, object), with {:ok, _} <- check_accept(uri),
{:ok, object} <- check_reject(uri, object) do {:ok, _} <- check_reject(uri) do
{:ok, object} {:ok, object}
else else
{:reject, nil} -> {:reject, "[SimplePolicy]"} {:reject, nil} -> {:reject, "[SimplePolicy]"}
@ -288,6 +344,7 @@ def describe do
mrf_simple_excluded = mrf_simple_excluded =
Config.get(:mrf_simple) Config.get(:mrf_simple)
|> Enum.filter(fn {_, v} -> is_list(v) end)
|> Enum.map(fn {rule, instances} -> |> Enum.map(fn {rule, instances} ->
{rule, Enum.reject(instances, fn {host, _} -> host in exclusions end)} {rule, Enum.reject(instances, fn {host, _} -> host in exclusions end)}
end) end)
@ -332,66 +389,78 @@ def config_description do
label: "MRF Simple", label: "MRF Simple",
description: "Simple ingress policies", description: "Simple ingress policies",
children: children:
[ ([
%{ %{
key: :media_removal, key: :media_removal,
description: description:
"List of instances to strip media attachments from and the reason for doing so" "List of instances to strip media attachments from and the reason for doing so"
}, },
%{ %{
key: :media_nsfw, key: :media_nsfw,
label: "Media NSFW", label: "Media NSFW",
description: description:
"List of instances to tag all media as NSFW (sensitive) from and the reason for doing so" "List of instances to tag all media as NSFW (sensitive) from and the reason for doing so"
}, },
%{ %{
key: :federated_timeline_removal, key: :federated_timeline_removal,
description: description:
"List of instances to remove from the Federated (aka The Whole Known Network) Timeline and the reason for doing so" "List of instances to remove from the Federated (aka The Whole Known Network) Timeline and the reason for doing so"
}, },
%{ %{
key: :reject, key: :reject,
description: description:
"List of instances to reject activities from (except deletes) and the reason for doing so" "List of instances to reject activities from (except deletes) and the reason for doing so"
}, },
%{ %{
key: :accept, key: :accept,
description: description:
"List of instances to only accept activities from (except deletes) and the reason for doing so" "List of instances to only accept activities from (except deletes) and the reason for doing so"
}, },
%{ %{
key: :followers_only, key: :followers_only,
description: description:
"Force posts from the given instances to be visible by followers only and the reason for doing so" "Force posts from the given instances to be visible by followers only and the reason for doing so"
}, },
%{ %{
key: :report_removal, key: :report_removal,
description: "List of instances to reject reports from and the reason for doing so" description: "List of instances to reject reports from and the reason for doing so"
}, },
%{ %{
key: :avatar_removal, key: :avatar_removal,
description: "List of instances to strip avatars from and the reason for doing so" description: "List of instances to strip avatars from and the reason for doing so"
}, },
%{ %{
key: :banner_removal, key: :banner_removal,
description: "List of instances to strip banners from and the reason for doing so" description: "List of instances to strip banners from and the reason for doing so"
}, },
%{ %{
key: :reject_deletes, key: :reject_deletes,
description: "List of instances to reject deletions from and the reason for doing so" description: "List of instances to reject deletions from and the reason for doing so"
} }
] ]
|> Enum.map(fn setting -> |> Enum.map(fn setting ->
Map.merge( Map.merge(
setting, setting,
%{
type: {:list, :tuple},
key_placeholder: "instance",
value_placeholder: "reason",
suggestions: [
{"example.com", "Some reason"},
{"*.example.com", "Another reason"}
]
}
)
end)) ++
[
%{ %{
type: {:list, :tuple}, key: :handle_threads,
key_placeholder: "instance", label: "Apply to entire threads",
value_placeholder: "reason", type: :boolean,
suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}] description:
"Enable to filter replies to threads based from their originating instance, using the reject and accept rules"
} }
) ]
end)
} }
end end
end end

View file

@ -356,6 +356,86 @@ test "reject by URI object" do
assert {:reject, _} = SimplePolicy.filter(announce) assert {:reject, _} = SimplePolicy.filter(announce)
end 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 end
describe "when :followers_only" do describe "when :followers_only" do