diff --git a/changelog.d/restrict-unauthenticated-account-statuses.fix b/changelog.d/restrict-unauthenticated-account-statuses.fix new file mode 100644 index 000000000..a6ae013fd --- /dev/null +++ b/changelog.d/restrict-unauthenticated-account-statuses.fix @@ -0,0 +1 @@ +Fixed account status and media views exposing activities restricted from unauthenticated users. diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index cdad6a49b..722bf8b60 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1357,6 +1357,43 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_unauthenticated(query, _), do: query + defp restrict_unauthenticated_object(query, nil) do + local = Config.restrict_unauthenticated_access?(:activities, :local) + remote = Config.restrict_unauthenticated_access?(:activities, :remote) + local_object_pattern = Pleroma.Web.Endpoint.url() <> "/%" + + cond do + local and remote -> + from([object: object] in query, where: false) + + local -> + from([object: object] in query, + where: not fragment("(?->>'id') LIKE ?", object.data, ^local_object_pattern) + ) + + remote -> + from([object: object] in query, + where: fragment("(?->>'id') LIKE ?", object.data, ^local_object_pattern) + ) + + true -> + query + end + end + + defp restrict_unauthenticated_object(query, _), do: query + + defp maybe_restrict_unauthenticated( + query, + %{restrict_unauthenticated: true, user: user} + ) do + query + |> restrict_unauthenticated(user) + |> restrict_unauthenticated_object(user) + end + + defp maybe_restrict_unauthenticated(query, _), do: query + defp restrict_quote_url(query, %{quote_url: quote_url}) do from([_activity, object] in query, where: fragment("(?)->'quoteUrl' = ?", object.data, ^quote_url) @@ -1514,6 +1551,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> maybe_preload_report_notes(opts) |> maybe_set_thread_muted_field(opts) |> maybe_order(opts) + |> maybe_restrict_unauthenticated(opts) |> restrict_recipients_or_hashtags(recipients, opts[:user], opts[:followed_hashtags]) |> restrict_replies(opts) |> restrict_since(opts) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 6d5851029..631102843 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -348,6 +348,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do params |> Map.delete(:tagged) |> Map.put(:tag, params[:tagged]) + |> Map.put(:restrict_unauthenticated, true) activities = ActivityPub.fetch_user_activities(user, reading_user, params) diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 01e195842..a2ac6382a 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -458,6 +458,112 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do assert [%{"id" => ^image_post_id}] = json_response_and_validate_schema(conn, 200) end + test "media respects activity restrictions when unauthenticated profiles are enabled", %{ + conn: conn + } do + clear_config([:instance, :public], false) + clear_config([:restrict_unauthenticated, :profiles, :local], false) + + user = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpeg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, %{id: media_id}} = ActivityPub.upload(file, actor: user.ap_id) + {:ok, activity} = CommonAPI.post(user, %{status: "cofe", media_ids: [media_id]}) + user_id = user.id + + assert %{"id" => ^user_id} = + build_conn() + |> get("/api/v1/accounts/#{user.id}") + |> json_response_and_validate_schema(200) + + assert [] = + build_conn() + |> get("/api/v1/accounts/#{user.id}/statuses?only_media=true") + |> json_response_and_validate_schema(200) + + assert [%{"id" => activity_id}] = + conn + |> get("/api/v1/accounts/#{user.id}/statuses?only_media=true") + |> json_response_and_validate_schema(200) + + assert activity_id == activity.id + end + + test "activity restrictions apply to boosted objects", %{conn: conn} do + clear_config([:restrict_unauthenticated, :activities, :local], false) + clear_config([:restrict_unauthenticated, :activities, :remote], true) + + local_user = insert(:user) + remote_user = insert(:user, local: false, domain: "example.com") + local_activity = insert(:note_activity, user: local_user) + + remote_activity = + insert(:note_activity, local: false, object_local: false, user: remote_user) + + {:ok, announce} = CommonAPI.repeat(remote_activity.id, local_user) + + status_ids = + build_conn() + |> get("/api/v1/accounts/#{local_user.id}/statuses") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["id"]) + + assert local_activity.id in status_ids + refute announce.id in status_ids + + authenticated_status_ids = + conn + |> get("/api/v1/accounts/#{local_user.id}/statuses") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["id"]) + + assert local_activity.id in authenticated_status_ids + assert announce.id in authenticated_status_ids + end + + test "local object restrictions apply to remote boosts", %{conn: conn} do + clear_config([:restrict_unauthenticated, :activities, :local], true) + clear_config([:restrict_unauthenticated, :activities, :remote], false) + + local_user = insert(:user) + remote_user = insert(:user, local: false, domain: "example.com") + local_activity = insert(:note_activity, user: local_user) + + remote_activity = + insert(:note_activity, local: false, object_local: false, user: remote_user) + + remote_announce = + insert(:announce_activity, note_activity: local_activity, user: remote_user) + |> Ecto.Changeset.change(%{ + local: false, + recipients: ["https://www.w3.org/ns/activitystreams#Public"] + }) + |> Repo.update!() + + status_ids = + build_conn() + |> get("/api/v1/accounts/#{remote_user.id}/statuses") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["id"]) + + assert remote_activity.id in status_ids + refute remote_announce.id in status_ids + + authenticated_status_ids = + conn + |> get("/api/v1/accounts/#{remote_user.id}/statuses") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["id"]) + + assert remote_activity.id in authenticated_status_ids + assert remote_announce.id in authenticated_status_ids + end + test "gets a user's statuses without reblogs", %{user: user, conn: conn} do {:ok, %{id: post_id}} = CommonAPI.post(user, %{status: "HI!!!"}) {:ok, _} = CommonAPI.repeat(post_id, user)