diff options
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | config/config.exs | 4 | ||||
-rw-r--r-- | docs/configuration/cheatsheet.md | 8 | ||||
-rw-r--r-- | lib/pleroma/plugs/oauth_plug.ex | 2 | ||||
-rw-r--r-- | lib/pleroma/plugs/user_enabled_plug.ex | 10 | ||||
-rw-r--r-- | lib/pleroma/user.ex | 3 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/mrf/object_age_policy.ex | 101 | ||||
-rw-r--r-- | lib/pleroma/web/router.ex | 1 | ||||
-rw-r--r-- | test/plugs/user_enabled_plug_test.exs | 17 | ||||
-rw-r--r-- | test/user_test.exs | 7 | ||||
-rw-r--r-- | test/web/activity_pub/mrf/object_age_policy_test.exs | 105 |
11 files changed, 255 insertions, 4 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index b4ad91b0d..a675fc426 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Static Frontend: Add the ability to render user profiles and notices server-side without requiring JS app. - Mix task to re-count statuses for all users (`mix pleroma.count_statuses`) - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache). +- MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers. <details> <summary>API Changes</summary> diff --git a/config/config.exs b/config/config.exs index 75f463797..bf2b3f6e2 100644 --- a/config/config.exs +++ b/config/config.exs @@ -381,6 +381,10 @@ config :pleroma, :mrf_vocabulary, accept: [], reject: [] +config :pleroma, :mrf_object_age, + threshold: 172_800, + actions: [:delist, :strip_followers] + config :pleroma, :rich_media, enabled: true, ignore_hosts: [], diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 7832f6962..d798bd692 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -41,6 +41,7 @@ You shouldn't edit the base config directly to avoid breakages and merge conflic * `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed. * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)). * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)). + * `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)). * `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send. * `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``. @@ -137,6 +138,13 @@ config :pleroma, :mrf_user_allowlist, "example.org": ["https://example.org/users/admin"] ``` +#### :mrf_object_age +* `threshold`: Required age (in seconds) of a post before actions are taken. +* `actions`: A list of actions to apply to the post: + * `:delist` removes the post from public timelines + * `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines + * `:reject` rejects the message entirely + ### :activitypub * ``unfollow_blocked``: Whether blocks result in people getting unfollowed * ``outgoing_blocks``: Whether to federate blocks to other instances diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex index fd004fcd2..11a5b7642 100644 --- a/lib/pleroma/plugs/oauth_plug.ex +++ b/lib/pleroma/plugs/oauth_plug.ex @@ -71,7 +71,7 @@ defmodule Pleroma.Plugs.OAuthPlug do ) # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength - with %Token{user: %{deactivated: false} = user} = token_record <- Repo.one(query) do + with %Token{user: user} = token_record <- Repo.one(query) do {:ok, user, token_record} end end diff --git a/lib/pleroma/plugs/user_enabled_plug.ex b/lib/pleroma/plugs/user_enabled_plug.ex index fbb4bf115..8d102ee5b 100644 --- a/lib/pleroma/plugs/user_enabled_plug.ex +++ b/lib/pleroma/plugs/user_enabled_plug.ex @@ -10,9 +10,13 @@ defmodule Pleroma.Plugs.UserEnabledPlug do options end - def call(%{assigns: %{user: %User{deactivated: true}}} = conn, _) do - conn - |> assign(:user, nil) + def call(%{assigns: %{user: %User{} = user}} = conn, _) do + if User.auth_active?(user) do + conn + else + conn + |> assign(:user, nil) + end end def call(conn, _) do diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index f8c2db1e1..fcb1d5143 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -124,6 +124,9 @@ defmodule Pleroma.User do timestamps() end + @doc "Returns if the user should be allowed to authenticate" + def auth_active?(%User{deactivated: true}), do: false + def auth_active?(%User{confirmation_pending: true}), do: !Pleroma.Config.get([:instance, :account_activation_required]) diff --git a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex new file mode 100644 index 000000000..8b36c1021 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex @@ -0,0 +1,101 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do + alias Pleroma.Config + alias Pleroma.User + alias Pleroma.Web.ActivityPub.MRF + + require Pleroma.Constants + + @moduledoc "Filter activities depending on their age" + @behaviour MRF + + defp check_date(%{"published" => published} = message) do + with %DateTime{} = now <- DateTime.utc_now(), + {:ok, %DateTime{} = then, _} <- DateTime.from_iso8601(published), + max_ttl <- Config.get([:mrf_object_age, :threshold]), + {:ttl, false} <- {:ttl, DateTime.diff(now, then) > max_ttl} do + {:ok, message} + else + {:ttl, true} -> + {:reject, nil} + + e -> + {:error, e} + end + end + + defp check_reject(message, actions) do + if :reject in actions do + {:reject, nil} + else + {:ok, message} + end + end + + defp check_delist(message, actions) do + if :delist in actions do + with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do + to = List.delete(message["to"], Pleroma.Constants.as_public()) ++ [user.follower_address] + cc = List.delete(message["cc"], user.follower_address) ++ [Pleroma.Constants.as_public()] + + message = + message + |> Map.put("to", to) + |> Map.put("cc", cc) + + {:ok, message} + else + # Unhandleable error: somebody is messing around, just drop the message. + _e -> + {:reject, nil} + end + else + {:ok, message} + end + end + + defp check_strip_followers(message, actions) do + if :strip_followers in actions do + with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do + to = List.delete(message["to"], user.follower_address) + cc = List.delete(message["cc"], user.follower_address) + + message = + message + |> Map.put("to", to) + |> Map.put("cc", cc) + + {:ok, message} + else + # Unhandleable error: somebody is messing around, just drop the message. + _e -> + {:reject, nil} + end + else + {:ok, message} + end + end + + @impl true + def filter(%{"type" => "Create", "published" => _} = message) do + with actions <- Config.get([:mrf_object_age, :actions]), + {:reject, _} <- check_date(message), + {:ok, message} <- check_reject(message, actions), + {:ok, message} <- check_delist(message, actions), + {:ok, message} <- check_strip_followers(message, actions) do + {:ok, message} + else + # check_date() is allowed to short-circuit the pipeline + e -> e + end + end + + @impl true + def filter(message), do: {:ok, message} + + @impl true + def describe, do: {:ok, %{}} +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 9b8b373b8..129da422c 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.Router do pipeline :oauth do plug(:fetch_session) plug(Pleroma.Plugs.OAuthPlug) + plug(Pleroma.Plugs.UserEnabledPlug) end pipeline :api do diff --git a/test/plugs/user_enabled_plug_test.exs b/test/plugs/user_enabled_plug_test.exs index 996a7d77b..a4035bf0e 100644 --- a/test/plugs/user_enabled_plug_test.exs +++ b/test/plugs/user_enabled_plug_test.exs @@ -16,6 +16,23 @@ defmodule Pleroma.Plugs.UserEnabledPlugTest do assert ret_conn == conn end + test "with a user that's not confirmed and a config requiring confirmation, it removes that user", + %{conn: conn} do + old = Pleroma.Config.get([:instance, :account_activation_required]) + Pleroma.Config.put([:instance, :account_activation_required], true) + + user = insert(:user, confirmation_pending: true) + + conn = + conn + |> assign(:user, user) + |> UserEnabledPlug.call(%{}) + + assert conn.assigns.user == nil + + Pleroma.Config.put([:instance, :account_activation_required], old) + end + test "with a user that is deactivated, it removes that user", %{conn: conn} do user = insert(:user, deactivated: true) diff --git a/test/user_test.exs b/test/user_test.exs index 6b1b24ce5..8fdb6b25f 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1195,6 +1195,13 @@ defmodule Pleroma.UserTest do refute User.auth_active?(local_user) assert User.auth_active?(confirmed_user) assert User.auth_active?(remote_user) + + # also shows unactive for deactivated users + + deactivated_but_confirmed = + insert(:user, local: true, confirmation_pending: false, deactivated: true) + + refute User.auth_active?(deactivated_but_confirmed) end describe "superuser?/1" do diff --git a/test/web/activity_pub/mrf/object_age_policy_test.exs b/test/web/activity_pub/mrf/object_age_policy_test.exs new file mode 100644 index 000000000..643609da4 --- /dev/null +++ b/test/web/activity_pub/mrf/object_age_policy_test.exs @@ -0,0 +1,105 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do + use Pleroma.DataCase + alias Pleroma.Config + alias Pleroma.User + alias Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy + alias Pleroma.Web.ActivityPub.Visibility + + clear_config([:mrf_object_age]) do + Config.put(:mrf_object_age, + threshold: 172_800, + actions: [:delist, :strip_followers] + ) + end + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "with reject action" do + test "it rejects an old post" do + Config.put([:mrf_object_age, :actions], [:reject]) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + {:reject, _} = ObjectAgePolicy.filter(data) + end + + test "it allows a new post" do + Config.put([:mrf_object_age, :actions], [:reject]) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601()) + + {:ok, _} = ObjectAgePolicy.filter(data) + end + end + + describe "with delist action" do + test "it delists an old post" do + Config.put([:mrf_object_age, :actions], [:delist]) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"]) + + {:ok, data} = ObjectAgePolicy.filter(data) + + assert Visibility.get_visibility(%{data: data}) == "unlisted" + end + + test "it allows a new post" do + Config.put([:mrf_object_age, :actions], [:delist]) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601()) + + {:ok, _user} = User.get_or_fetch_by_ap_id(data["actor"]) + + {:ok, ^data} = ObjectAgePolicy.filter(data) + end + end + + describe "with strip_followers action" do + test "it strips followers collections from an old post" do + Config.put([:mrf_object_age, :actions], [:strip_followers]) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + {:ok, user} = User.get_or_fetch_by_ap_id(data["actor"]) + + {:ok, data} = ObjectAgePolicy.filter(data) + + refute user.follower_address in data["to"] + refute user.follower_address in data["cc"] + end + + test "it allows a new post" do + Config.put([:mrf_object_age, :actions], [:strip_followers]) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601()) + + {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"]) + + {:ok, ^data} = ObjectAgePolicy.filter(data) + end + end +end |