Merge branch 'from/upstream-develop/tusooa/report-anon' into 'develop'

Anonymize reports

Closes #2661 and #1024

See merge request pleroma/pleroma!3806
This commit is contained in:
lain 2025-09-05 12:15:19 +00:00
commit d1d7dd1827
19 changed files with 376 additions and 73 deletions

View file

@ -0,0 +1 @@
Fix reports being rejected when the activity had an empty CC or TO field (instead of not having them at all)

View file

@ -0,0 +1 @@
Allow anonymizing reports sent to remote servers

View file

@ -364,7 +364,9 @@ config :pleroma, :activitypub,
note_replies_output_limit: 5,
sign_object_fetches: true,
authorized_fetch_mode: false,
client_api_enabled: false
client_api_enabled: false,
anonymize_reporter: false,
anonymize_reporter_local_nickname: ""
config :pleroma, :streamer,
workers: 3,

View file

@ -1797,6 +1797,23 @@ config :pleroma, :config_description, [
key: :client_api_enabled,
type: :boolean,
description: "Allow client to server ActivityPub interactions"
},
%{
key: :anonymize_reporter,
type: :boolean,
label: "Anonymize local reports",
description:
"If true, replace local reporters with the designated local user for the copy to be sent to remote servers"
},
%{
key: :anonymize_reporter_local_nickname,
type: :string,
label: "Anonymized reporter",
description:
"The nickname of the designated local user that replaces the actual reporter in the copy to be sent to remote servers",
suggestions: [
"lain"
]
}
]
},

View file

@ -170,6 +170,10 @@ config :pleroma, Pleroma.Upload.Filter.Mogrify, config_impl: Pleroma.StaticStubb
config :pleroma, Pleroma.Upload.Filter.Mogrify, mogrify_impl: Pleroma.MogrifyMock
config :pleroma, Pleroma.Signature, http_signatures_impl: Pleroma.StubbedHTTPSignaturesMock
config :pleroma, Pleroma.Web.ActivityPub.Publisher, signature_impl: Pleroma.SignatureMock
config :pleroma, Pleroma.Web.ActivityPub.Publisher,
transmogrifier_impl: Pleroma.Web.ActivityPub.TransmogrifierMock
peer_module =
if String.to_integer(System.otp_release()) >= 25 do

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Signature do
@behaviour Pleroma.Signature.API
@behaviour HTTPSignatures.Adapter
alias Pleroma.EctoType.ActivityPub.ObjectValidators

View file

@ -0,0 +1,14 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Signature.API do
@moduledoc """
Behaviour for signing requests and producing HTTP Date headers.
This is used to allow tests to replace the signing implementation with Mox.
"""
@callback sign(user :: Pleroma.User.t(), headers :: map()) :: String.t()
@callback signed_date() :: String.t()
end

View file

@ -414,10 +414,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
with flag_data <- make_flag_data(params, additional),
{:ok, activity} <- insert(flag_data, local),
{:ok, stripped_activity} <- strip_report_status_data(activity),
_ <- notify_and_stream(activity),
:ok <-
maybe_federate(stripped_activity) do
:ok <- maybe_federate(activity) do
User.all_users_with_privilege(:reports_manage_reports)
|> Enum.filter(fn user -> user.ap_id != actor end)
|> Enum.filter(fn user -> not is_nil(user.email) end)

View file

@ -13,7 +13,6 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Publisher.Prepared
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Workers.PublisherWorker
require Pleroma.Constants
@ -26,6 +25,18 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
ActivityPub outgoing federation module.
"""
@signature_impl Application.compile_env(
:pleroma,
[__MODULE__, :signature_impl],
Pleroma.Signature
)
@transmogrifier_impl Application.compile_env(
:pleroma,
[__MODULE__, :transmogrifier_impl],
Pleroma.Web.ActivityPub.Transmogrifier
)
@doc """
Enqueue publishing a single activity.
"""
@ -68,7 +79,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
Determine if an activity can be represented by running it through Transmogrifier.
"""
def representable?(%Activity{} = activity) do
with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
with {:ok, _data} <- @transmogrifier_impl.prepare_outgoing(activity.data) do
true
else
_e ->
@ -91,7 +102,17 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
Logger.debug("Federating #{ap_id} to #{inbox}")
uri = %{path: path} = URI.parse(inbox)
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, data} = @transmogrifier_impl.prepare_outgoing(activity.data)
{actor, data} =
with {_, false} <- {:actor_changed?, data["actor"] != activity.data["actor"]} do
{actor, data}
else
{:actor_changed?, true} ->
# If prepare_outgoing changes the actor, re-get it from the db
new_actor = User.get_cached_by_ap_id(data["actor"])
{new_actor, data}
end
param_cc = Map.get(params, :cc, [])
@ -115,10 +136,10 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
date = Pleroma.Signature.signed_date()
date = @signature_impl.signed_date()
signature =
Pleroma.Signature.sign(actor, %{
@signature_impl.sign(actor, %{
"(request-target)": "post #{path}",
host: signature_host(uri),
"content-length": byte_size(json),

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
@moduledoc """
A module to handle coding from internal to wire ActivityPub and back.
"""
@behaviour Pleroma.Web.ActivityPub.Transmogrifier.API
alias Pleroma.Activity
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Maps
@ -909,6 +910,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
def prepare_outgoing(%{"type" => "Flag"} = data) do
with {:ok, stripped_activity} <- Utils.strip_report_status_data(data),
stripped_activity <- Utils.maybe_anonymize_reporter(stripped_activity),
stripped_activity <- Map.merge(stripped_activity, Utils.make_json_ld_header()) do
{:ok, stripped_activity}
end
end
def prepare_outgoing(%{"type" => _type} = data) do
data =
data

View file

@ -0,0 +1,11 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.API do
@moduledoc """
Behaviour for the subset of Transmogrifier used by Publisher.
"""
@callback prepare_outgoing(map()) :: {:ok, map()} | {:error, term()}
end

View file

@ -82,7 +82,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do
def unaddressed_message?(params),
do:
[params["to"], params["cc"], params["bto"], params["bcc"]]
|> Enum.all?(&is_nil(&1))
|> Enum.all?(fn
nil -> true
[] -> true
_ -> false
end)
@spec recipient_in_message(User.t(), User.t(), map()) :: boolean()
def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params),
@ -859,8 +863,14 @@ defmodule Pleroma.Web.ActivityPub.Utils do
def update_report_state(_, _), do: {:error, "Unsupported state"}
def strip_report_status_data(activity) do
[actor | reported_activities] = activity.data["object"]
def strip_report_status_data(%Activity{} = activity) do
with {:ok, new_data} <- strip_report_status_data(activity.data) do
{:ok, %{activity | data: new_data}}
end
end
def strip_report_status_data(data) do
[actor | reported_activities] = data["object"]
stripped_activities =
Enum.reduce(reported_activities, [], fn act, acc ->
@ -870,9 +880,36 @@ defmodule Pleroma.Web.ActivityPub.Utils do
end
end)
new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
new_data = put_in(data, ["object"], [actor | stripped_activities])
{:ok, %{activity | data: new_data}}
{:ok, new_data}
end
def get_anonymized_reporter do
with true <- Pleroma.Config.get([:activitypub, :anonymize_reporter]),
nickname when is_binary(nickname) <-
Pleroma.Config.get([:activitypub, :anonymize_reporter_local_nickname]),
%User{ap_id: ap_id, local: true} <- User.get_cached_by_nickname(nickname) do
ap_id
else
_ -> nil
end
end
def maybe_anonymize_reporter(%Activity{data: data} = activity) do
new_data = maybe_anonymize_reporter(data)
%Activity{activity | actor: new_data["actor"], data: new_data}
end
def maybe_anonymize_reporter(activity) do
ap_id = get_anonymized_reporter()
if is_binary(ap_id) do
activity
|> Map.put("actor", ap_id)
else
activity
end
end
def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do

View file

@ -1152,9 +1152,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
}
],
"actor" => actor.ap_id,
"cc" => [
reported_user.ap_id
],
# CC and TO might either not exist at all, or be empty. We should be able to handle either.
# "cc" => [],
"content" => "test",
"context" => "context",
"id" => "http://#{remote_domain}/activities/02be56cf-35e3-46b4-b2c6-47ae08dfee9e",

View file

@ -1691,32 +1691,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
} = activity
end
test_with_mock "strips status data from Flag, before federating it",
%{
reporter: reporter,
context: context,
target_account: target_account,
reported_activity: reported_activity,
object_ap_id: object_ap_id,
content: content
},
Utils,
[:passthrough],
[] do
{:ok, activity} =
ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
new_data = put_in(activity.data, ["object"], [target_account.ap_id, object_ap_id])
assert_called(Utils.maybe_federate(%{activity | data: new_data}))
end
test_with_mock "reverts on error",
%{
reporter: reporter,

View file

@ -8,12 +8,13 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
import Pleroma.Factory
import Tesla.Mock
import Mock
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Tests.ObanHelpers
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
@as_public "https://www.w3.org/ns/activitystreams#Public"
@ -168,10 +169,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
end
describe "publish/2" do
test_with_mock "doesn't publish a non-public activity to quarantined instances.",
Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
test "doesn't publish a non-public activity to quarantined instances." do
Config.put([:instance, :quarantined_instances], [{"domain.com", "some reason"}])
follower =
@ -206,10 +204,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
)
end
test_with_mock "Publishes a non-public activity to non-quarantined instances.",
Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
test "Publishes a non-public activity to non-quarantined instances." do
Config.put([:instance, :quarantined_instances], [{"somedomain.com", "some reason"}])
follower =
@ -245,10 +240,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
)
end
test_with_mock "Publishes to directly addressed actors with higher priority.",
Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
test "Publishes to directly addressed actors with higher priority." do
note_activity = insert(:direct_note_activity)
actor = Pleroma.User.get_by_ap_id(note_activity.data["actor"])
@ -257,21 +249,58 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
assert res == :ok
assert called(
Publisher.enqueue_one(
%{
inbox: :_,
activity_id: note_activity.id
},
priority: 0
)
)
assert_enqueued(
worker: "Pleroma.Workers.PublisherWorker",
args: %{
"op" => "publish_one",
"params" => %{"activity_id" => note_activity.id}
},
priority: 0
)
end
test_with_mock "publishes an activity with BCC to all relevant peers.",
Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
test "Publishes with the new actor if prepare_outgoing changes the actor." do
mock(fn
%{method: :post, url: "https://domain.com/users/nick1/inbox", body: body} ->
{:ok, %Tesla.Env{status: 200, body: body}}
end)
other_user =
insert(:user, %{
local: false,
inbox: "https://domain.com/users/nick1/inbox"
})
actor = insert(:user)
replaced_actor = insert(:user)
note_activity =
insert(:note_activity,
user: actor,
data_attrs: %{"to" => [other_user.ap_id]}
)
Pleroma.Web.ActivityPub.TransmogrifierMock
|> Mox.expect(:prepare_outgoing, fn data ->
{:ok, Map.put(data, "actor", replaced_actor.ap_id)}
end)
prepared =
Publisher.prepare_one(%{
inbox: "https://domain.com/users/nick1/inbox",
activity_id: note_activity.id,
cc: ["https://domain.com/users/nick2/inbox"]
})
{:ok, decoded} = Jason.decode(prepared.json)
assert decoded["actor"] == replaced_actor.ap_id
{:ok, published} = Publisher.publish_one(prepared)
sent_activity = Jason.decode!(published.body)
assert sent_activity["actor"] == replaced_actor.ap_id
end
test "publishes an activity with BCC to all relevant peers." do
follower =
insert(:user, %{
local: false,
@ -303,10 +332,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
)
end
test_with_mock "publishes a delete activity to peers who signed fetch requests to the create acitvity/object.",
Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
test "publishes a delete activity to peers who signed fetch requests to the create acitvity/object." do
fetcher =
insert(:user,
local: false,
@ -510,4 +536,43 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
assert decoded["cc"] == ["https://example.com/specific/user"]
end
describe "prepare_one/1 with reporter anonymization" do
test "signs with the anonymized actor keys when Transmogrifier changes actor" do
Pleroma.SignatureMock
|> Mox.stub(:signed_date, fn -> Pleroma.Signature.signed_date() end)
|> Mox.expect(:sign, fn %Pleroma.User{} = user, _headers ->
send(self(), {:signed_as, user.ap_id})
"TESTSIG"
end)
placeholder = insert(:user)
reporter = insert(:user)
target_account = insert(:user)
clear_config([:activitypub, :anonymize_reporter], true)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
{:ok, reported} = CommonAPI.post(target_account, %{status: "content"})
context = Utils.generate_context_id()
{:ok, activity} =
ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported],
content: "reason"
})
_prepared =
Publisher.prepare_one(%{
inbox: "http://remote.example/users/alice/inbox",
activity_id: activity.id
})
assert_received {:signed_as, ap_id}
assert ap_id == placeholder.ap_id
end
end
end

View file

@ -642,6 +642,69 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert [_, _, %{"@language" => "pl"}] = modified["@context"]
end
test "it strips report data" do
reporter = insert(:user)
target_account = insert(:user)
content = "foobar"
{:ok, reported_activity} = CommonAPI.post(target_account, %{status: content})
context = Utils.generate_context_id()
object_ap_id = reported_activity.object.data["id"]
assert {:ok, activity} =
Pleroma.Web.ActivityPub.ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
expected_data =
activity.data
|> put_in(["object"], [target_account.ap_id, object_ap_id])
|> Map.put("actor", reporter.ap_id)
|> Map.merge(Utils.make_json_ld_header())
assert data == expected_data
end
test "it strips report data and anonymize" do
placeholder = insert(:user)
reporter = insert(:user)
target_account = insert(:user)
content = "foobar"
{:ok, reported_activity} = CommonAPI.post(target_account, %{status: content})
context = Utils.generate_context_id()
object_ap_id = reported_activity.object.data["id"]
assert {:ok, activity} =
Pleroma.Web.ActivityPub.ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
clear_config([:activitypub, :anonymize_reporter], true)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
expected_data =
activity.data
|> put_in(["object"], [target_account.ap_id, object_ap_id])
|> Map.put("actor", placeholder.ap_id)
|> Map.merge(Utils.make_json_ld_header())
assert data == expected_data
end
end
describe "actor rewriting" do

View file

@ -670,4 +670,78 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
)
end
end
describe "maybe_anonymize_reporter/1" do
setup do
reporter = insert(:user)
report = %{"actor" => reporter.ap_id}
%{
placeholder: insert(:user),
reporter: reporter,
report: report
}
end
test "anonymize when configured correctly", %{
placeholder: placeholder,
report: report
} do
clear_config([:activitypub, :anonymize_reporter], true)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
assert %{"actor" => placeholder.ap_id} == Utils.maybe_anonymize_reporter(report)
end
test "anonymize Activity", %{
placeholder: placeholder,
reporter: reporter,
report: report
} do
clear_config([:activitypub, :anonymize_reporter], true)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
report_activity = %Activity{actor: reporter, data: report}
anon_id = placeholder.ap_id
assert %Activity{actor: ^anon_id, data: %{"actor" => ^anon_id}} =
Utils.maybe_anonymize_reporter(report_activity)
end
test "do not anonymize when disabled", %{
placeholder: placeholder,
reporter: reporter,
report: report
} do
clear_config([:activitypub, :anonymize_reporter], false)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
assert %{"actor" => reporter.ap_id} == Utils.maybe_anonymize_reporter(report)
end
test "do not anonymize when user does not exist", %{
placeholder: placeholder,
reporter: reporter,
report: report
} do
clear_config([:activitypub, :anonymize_reporter], true)
clear_config(
[:activitypub, :anonymize_reporter_local_nickname],
placeholder.nickname <> "MewMew"
)
assert %{"actor" => reporter.ap_id} == Utils.maybe_anonymize_reporter(report)
end
test "do not anonymize when user is not local", %{
reporter: reporter,
report: report
} do
placeholder = insert(:user, local: false)
clear_config([:activitypub, :anonymize_reporter], true)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
assert %{"actor" => reporter.ap_id} == Utils.maybe_anonymize_reporter(report)
end
end
end

View file

@ -119,6 +119,12 @@ defmodule Pleroma.DataCase do
Mox.stub_with(Pleroma.StubbedHTTPSignaturesMock, Pleroma.Test.HTTPSignaturesProxy)
Mox.stub_with(Pleroma.DateTimeMock, Pleroma.DateTime.Impl)
Mox.stub_with(Pleroma.SignatureMock, Pleroma.Signature)
Mox.stub_with(
Pleroma.Web.ActivityPub.TransmogrifierMock,
Pleroma.Web.ActivityPub.Transmogrifier
)
end
def ensure_local_uploader(context) do

View file

@ -40,3 +40,9 @@ Mox.defmock(Pleroma.Language.LanguageDetectorMock,
Mox.defmock(Pleroma.DateTimeMock, for: Pleroma.DateTime)
Mox.defmock(Pleroma.MogrifyMock, for: Pleroma.MogrifyBehaviour)
Mox.defmock(Pleroma.SignatureMock, for: Pleroma.Signature.API)
Mox.defmock(Pleroma.Web.ActivityPub.TransmogrifierMock,
for: Pleroma.Web.ActivityPub.Transmogrifier.API
)