From 59fcb5c96ebebad6973f294d47ec6f7ed75b38bd Mon Sep 17 00:00:00 2001 From: Oneric Date: Mon, 1 Dec 2025 00:06:13 +0100 Subject: [PATCH] api: ensure only visible posts are interactable Port of Akkoma PR 1014 with a few changes: - comments regarding akkomafe changed to Pleroma-FE when applicable - different error message for replying to/interacting with invisible post in Pleroma.Web.CommonAPI.ActivityDraft.in_reply_to/1 - split "doesn't do funny things to other users favs" test into three: - can't unfavourite post that isn't favourited - can't unfavourite other user's favs - can't unfavourite other user's favs using their activity - switched order of args for some CommonAPI function since Akkoma hasn't backported our old change for that Pleroma.Web.CommonAPI.ActivityDraft.in_reply_to/1 now refactored to use `with` statement as in Akkoma. Some defp in_reply_to/1 were therefore removed Original PR author: Oneric Original commit message: It doesn't make sense to like, react, reply, etc to something you cannot see and is unexpected for the author of the interacted with post and might make them believe the reacting user actually _can_ see the post. Wrt to fav, reblog, reaction indexes the missing visibility check was also leaking some (presumably/hopefully) low-severity data. Add full-API test for all modes of interactions with private posts. --- .../operations/emoji_reaction_operation.ex | 3 +- .../api_spec/operations/status_operation.ex | 2 + lib/pleroma/web/common_api.ex | 10 +- lib/pleroma/web/common_api/activity_draft.ex | 35 +-- .../controllers/status_controller.ex | 3 + .../controllers/emoji_reaction_controller.ex | 5 + .../conversation/participation_test.exs | 2 +- test/pleroma/conversation_test.exs | 10 +- .../notification_controller_test.exs | 3 + .../controllers/status_controller_test.exs | 237 ++++++++++++++++++ .../emoji_reaction_controller_test.exs | 108 ++++++++ 11 files changed, 398 insertions(+), 20 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex index 8d6be89a7..60fe8f011 100644 --- a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex +++ b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex @@ -35,7 +35,8 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do security: [%{"oAuth" => ["read:statuses"]}], operationId: "EmojiReactionController.index", responses: %{ - 200 => array_of_reactions_response() + 200 => array_of_reactions_response(), + 403 => Operation.response("Access denied", "application/json", ApiError) } } end diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 500fbb64e..daf473fa0 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -162,6 +162,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do parameters: [id_param()], responses: %{ 200 => status_response(), + 403 => Operation.response("Access denied", "application/json", ApiError), 404 => Operation.response("Not Found", "application/json", ApiError) } } @@ -388,6 +389,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do "application/json", AccountOperation.array_of_accounts() ), + 403 => Operation.response("Access denied", "application/json", ApiError), 404 => Operation.response("Not Found", "application/json", ApiError) } } diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index ae554d0b9..c1719c8d4 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -258,7 +258,7 @@ defmodule Pleroma.Web.CommonAPI do {:ok, _} = res -> res - {:error, :not_found} = res -> + {:error, reason} = res when reason in [:not_found, :forbidden] -> res {:error, e} -> @@ -269,6 +269,7 @@ defmodule Pleroma.Web.CommonAPI do defp favorite_helper(user, id) do with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)}, + {_, true} <- {:visible, Visibility.visible_for_user?(object, user)}, {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)}, {_, {:ok, %Activity{} = activity, _meta}} <- {:common_pipeline, @@ -278,6 +279,9 @@ defmodule Pleroma.Web.CommonAPI do {:find_object, _} -> {:error, :not_found} + {:visible, _} -> + {:error, :forbidden} + {:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e -> if {:object, {"already liked by this actor", []}} in changeset.errors do {:ok, :already_liked} @@ -311,11 +315,15 @@ defmodule Pleroma.Web.CommonAPI do {:ok, Activity.t()} | {:error, String.t()} def react_with_emoji(id, user, emoji) do with %Activity{} = activity <- Activity.get_by_id(id), + {_, true} <- {:visible, Visibility.visible_for_user?(activity, user)}, object <- Object.normalize(activity, fetch: false), {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji), {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do {:ok, activity} else + {:visible, _} -> + {:error, dgettext("errors", "Must be able to access post to interact with it")} + _ -> {:error, dgettext("errors", "Could not add reaction emoji")} end diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index f60ed8b02..2d212a443 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -136,22 +136,29 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft - defp in_reply_to(%{params: %{in_reply_to_status_id: :deleted}} = draft) do - add_error(draft, dgettext("errors", "Cannot reply to a deleted status")) - end + defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do + # If a post was deleted all its activities (except the newly added Delete) are purged too, + # thus lookup by Create db ID will yield nil just as if it never existed in the first place. + # + # We allow replying to Announce here, due to a Pleroma-FE quirk where if presented with an Announce id + # it will render it as if it was just the normal referenced post, but use the Announce id for replies + # in the in_reply_to_id key of a POST request to /api/v1/statuses, or as an :id in /api/v1/statuses/:id/*. + # TODO: Fix this quirk in FE and remove here and other affected places + with %Activity{} = activity <- Activity.get_by_id(id), + true <- Visibility.visible_for_user?(activity, draft.user), + {:type, type} when type in ["Create", "Announce"] <- {:type, activity.data["type"]} do + %__MODULE__{draft | in_reply_to: activity} + else + nil -> + add_error(draft, dgettext("errors", "Cannot reply to a deleted status")) - defp in_reply_to(%{params: %{in_reply_to_status_id: id} = params} = draft) when is_binary(id) do - activity = Activity.get_by_id(id) + false -> + add_error(draft, dgettext("errors", "Replying to a status that is not visibile to user")) - params = - if is_nil(activity) do - # Deleted activities are returned as nil - Map.put(params, :in_reply_to_status_id, :deleted) - else - Map.put(params, :in_reply_to_status_id, activity) - end - - in_reply_to(%{draft | params: params}) + {:type, type} -> + add_error(draft, dgettext("errors", "Can only reply to posts, not %{type} activities", + type: inspect(type))) + end end defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 218ee7e28..62cc6a670 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -319,6 +319,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do @doc "DELETE /api/v1/statuses/:id" def delete(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do with %Activity{} = activity <- Activity.get_by_id_with_object(id), + # CommonAPI already checks whether user is allowed to delete {:ok, %Activity{}} <- CommonAPI.delete(id, user) do try_render(conn, "show.json", activity: activity, @@ -340,6 +341,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do _ ) do with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params), + # CommonAPI already checks whether user is allowed to reblog %Activity{} = announce <- Activity.normalize(announce.data) do try_render(conn, "show.json", %{activity: announce, for: user, as: :activity}) end @@ -364,6 +366,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do _ ) do with {:ok, _fav} <- CommonAPI.favorite(activity_id, user), + # CommonAPI already checks whether user is allowed to reblog %Activity{} = activity <- Activity.get_by_id(activity_id) do try_render(conn, "show.json", activity: activity, for: user, as: :activity) end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex index 662cc15d6..472c3742a 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do alias Pleroma.Activity alias Pleroma.Object alias Pleroma.User + alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.Plugs.OAuthScopesPlug @@ -28,6 +29,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do with true <- Pleroma.Config.get([:instance, :show_reactions]), %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), + {_, true} <- {:visible, Visibility.visible_for_user?(activity, user)}, %Object{} = object <- Object.normalize(activity, fetch: false), reactions <- Object.get_emoji_reactions(object) do reactions = @@ -37,6 +39,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do render(conn, "index.json", emoji_reactions: reactions, user: user) else + {:visible, _} -> {:error, :forbidden} _e -> json(conn, []) end end @@ -76,6 +79,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do |> Pleroma.Emoji.fully_qualify_emoji() |> Pleroma.Emoji.maybe_quote() + # CommonAPI checks if allowed to react with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do activity = Activity.get_by_id(activity_id) @@ -91,6 +95,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do |> Pleroma.Emoji.fully_qualify_emoji() |> Pleroma.Emoji.maybe_quote() + # CommonAPI checks only author can revoke reactions with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do activity = Activity.get_by_id(activity_id) diff --git a/test/pleroma/conversation/participation_test.exs b/test/pleroma/conversation/participation_test.exs index 697bdb7f9..f6fde1875 100644 --- a/test/pleroma/conversation/participation_test.exs +++ b/test/pleroma/conversation/participation_test.exs @@ -332,7 +332,7 @@ defmodule Pleroma.Conversation.ParticipationTest do # When it's a reply from the blocked user {:ok, _direct2} = CommonAPI.post(blocked, %{ - status: "reply", + status: "@#{third_user.nickname}, #{blocker.nickname} reply", visibility: "direct", in_reply_to_conversation_id: blocked_participation.id }) diff --git a/test/pleroma/conversation_test.exs b/test/pleroma/conversation_test.exs index 02b5de615..bc41165ed 100644 --- a/test/pleroma/conversation_test.exs +++ b/test/pleroma/conversation_test.exs @@ -66,8 +66,10 @@ defmodule Pleroma.ConversationTest do jafnhar = insert(:user, local: false) tridi = insert(:user) + to = [har.nickname, jafnhar.nickname, tridi.nickname] + {:ok, activity} = - CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "direct"}) + CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "direct", to: to}) object = Pleroma.Object.normalize(activity, fetch: false) context = object.data["context"] @@ -88,7 +90,8 @@ defmodule Pleroma.ConversationTest do CommonAPI.post(jafnhar, %{ status: "Hey @#{har.nickname}", visibility: "direct", - in_reply_to_status_id: activity.id + in_reply_to_status_id: activity.id, + to: to }) object = Pleroma.Object.normalize(activity, fetch: false) @@ -112,7 +115,8 @@ defmodule Pleroma.ConversationTest do CommonAPI.post(tridi, %{ status: "Hey @#{har.nickname}", visibility: "direct", - in_reply_to_status_id: activity.id + in_reply_to_status_id: activity.id, + to: to }) object = Pleroma.Object.normalize(activity, fetch: false) diff --git a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs index 88f2fb7af..dd4b7389e 100644 --- a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs @@ -316,6 +316,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do user = insert(:user) %{user: other_user, conn: conn} = oauth_access(["read:notifications"]) + {:ok, _, _, %{data: %{"state" => "accept"}}} = CommonAPI.follow(other_user, user) + {:ok, _, _, %{data: %{"state" => "accept"}}} = CommonAPI.follow(user, other_user) + {:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"}) {:ok, direct_activity} = diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index c19a6ca4e..d8558465a 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI alias Pleroma.Workers.ScheduledActivityWorker @@ -267,6 +268,72 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do end) end + test "replying to a post the current user can't access fails", %{user: user, conn: conn} do + stranger = insert(:user) + + {:ok, priv_post_act} = + CommonAPI.post(stranger, %{status: "forbidden knowledge", visibility: "private"}) + + assert Visibility.visible_for_user?(priv_post_act, stranger) + refute Visibility.visible_for_user?(priv_post_act, user) + + resp = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "@#{stranger.nickname} :peek:", + "in_reply_to_id" => priv_post_act.id, + "visibility" => "private" + }) + |> json_response_and_validate_schema(422) + + assert match?(%{"error" => _}, resp) + end + + test "replying to own DM succeeds", %{user: user, conn: conn} do + # this is an "edge" case for visibility: replying user is not part of addressed users (but is the author) + stranger = insert(:user) + + {:ok, %{id: dm_id} = dm_post_act} = + CommonAPI.post(user, %{ + status: "@#{stranger.nickname} wanna lose your mind to forbidden knowledge?", + visibility: "direct" + }) + + assert Visibility.visible_for_user?(dm_post_act, stranger) + assert Visibility.visible_for_user?(dm_post_act, user) + + resp = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "@#{stranger.nickname} :peek:", + "in_reply_to_id" => dm_id, + "visibility" => "direct" + }) + |> json_response_and_validate_schema(200) + + assert match?(%{"in_reply_to_id" => ^dm_id}, resp) + end + + test "replying to a non-post activity fails", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user) + assert Visibility.visible_for_user?(follow_activity, user) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "hiiii!", + "in_reply_to_id" => to_string(follow_activity.id) + }) + + assert %{"error" => "Can only reply to posts, not \"Follow\" activities"} = + json_response_and_validate_schema(conn, 422) + end + test "posting a status with an invalid in_reply_to_id", %{conn: conn} do conn = conn @@ -1416,6 +1483,24 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do assert to_string(activity.id) == id end + + test "cannot reblog private status of others (even if visible)", %{conn: conn, user: user} do + followed = insert(:user, local: true) + + {:ok, _, _, %{data: %{"state" => "accept"}}} = CommonAPI.follow(followed, user) + + {:ok, activity} = CommonAPI.post(followed, %{status: "cofe", visibility: "private"}) + + assert Visibility.visible_for_user?(activity, user) + + resp = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/reblog") + |> json_response_and_validate_schema(404) + + assert match?(%{"error" => _}, resp) + end end describe "unreblogging" do @@ -1445,6 +1530,33 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} end + + test "can't unreblog someone else's reblog", %{user: user, conn: conn} do + activity = insert(:note_activity) + other_user = insert(:user) + + {:ok, %{id: reblog_id}} = CommonAPI.repeat(activity.id, other_user) + + # unreblog by base post + resp1 = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/unreblog") + |> json_response(400) + + assert match?(%{"error" => _}, resp1) + + # unreblog by reblog ID (reblog IDs are accepted by some APIs; ensure it fails here one way or another) + resp2 = + build_conn() + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write", "read"])) + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{reblog_id}/unreblog") + |> json_response_and_validate_schema(404) + + assert match?(%{"error" => _}, resp2) + end end describe "favoriting" do @@ -1477,6 +1589,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do |> json_response_and_validate_schema(200) end + test "a status you cannot see fails", %{conn: conn} do + stranger = insert(:user) + + {:ok, activity} = + CommonAPI.post(stranger, %{status: "it can eternal lie", visibility: "private"}) + + resp = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/favourite") + |> json_response_and_validate_schema(403) + + assert match?(%{"error" => _}, resp) + end + test "returns 404 error for a wrong id", %{conn: conn} do conn = conn @@ -1506,6 +1633,50 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do assert to_string(activity.id) == id end + test "can't unfavourite post that isn't favourited", %{conn: conn} do + activity = insert(:note_activity) + + # using base post ID + resp = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/unfavourite") + |> json_response(400) + + assert match?(%{"error" => "Could not unfavorite"}, resp) + end + + test "can't unfavourite other user's favs", %{conn: conn} do + activity = insert(:note_activity) + + other = insert(:user) + {:ok, _} = CommonAPI.favorite(activity.id, other) + + # using base post ID + resp = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/unfavourite") + |> json_response(400) + + assert match?(%{"error" => "Could not unfavorite"}, resp) + end + + test "can't unfavourite other user's favs using their activity", %{conn: conn} do + activity = insert(:note_activity) + + other = insert(:user) + {:ok, fav_activity} = CommonAPI.favorite(activity.id, other) + # some APIs (used to) take IDs of any activity type, make sure this fails one way or another + resp = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{fav_activity.id}/unfavourite") + |> json_response_and_validate_schema(404) + + assert match?(%{"error" => _}, resp) + end + test "returns 404 error for a wrong id", %{conn: conn} do conn = conn @@ -1959,6 +2130,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do assert id == other_user.id end + test "fails when base post not visible to current user", %{user: user} do + other_user = insert(:user, local: true) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "craving tea and mochi rn", + visibility: "private" + }) + + resp = + build_conn() + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) + |> get("/api/v1/statuses/#{activity.id}/favourited_by") + |> json_response_and_validate_schema(404) + + assert match?(%{"error" => _}, resp) + end + test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do clear_config([:instance, :show_reactions], false) @@ -2077,6 +2267,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do assert [] == response end + + test "does fail when requesting for a non-visible status", %{user: user} do + other_user = insert(:user, local: true) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "deep below it sleeps and mustn't wake", + visibility: "private" + }) + + response = + build_conn() + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read"])) + |> get("/api/v1/statuses/#{activity.id}/reblogged_by") + |> json_response_and_validate_schema(404) + + assert match?(%{"error" => _}, response) + end end test "context" do @@ -2099,6 +2308,34 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do } = response end + test "context doesn't leak priv posts" do + %{user: user, conn: conn} = oauth_access(["read:statuses"]) + stranger = insert(:user) + + {:ok, %{id: id1}} = CommonAPI.post(stranger, %{status: "1", visibility: "public"}) + + {:ok, %{id: id2}} = + CommonAPI.post(stranger, %{status: "2", visibility: "unlisted", in_reply_to_status_id: id1}) + + {:ok, %{id: _id_boo} = act_boo} = + CommonAPI.post(stranger, %{status: "boo", visibility: "private", in_reply_to_status_id: id1}) + + refute Visibility.visible_for_user?(act_boo, user) + + response = + conn + |> get("/api/v1/statuses/#{id1}/context") + |> json_response_and_validate_schema(:ok) + + assert match?( + %{ + "ancestors" => [], + "descendants" => [%{"id" => ^id2}] + }, + response + ) + end + test "favorites paginate correctly" do %{user: user, conn: conn} = oauth_access(["read:favourites"]) other_user = insert(:user) diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs index c1e452a1e..f74f5ebc0 100644 --- a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs @@ -9,10 +9,38 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do alias Pleroma.Object alias Pleroma.Tests.ObanHelpers alias Pleroma.User + alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI import Pleroma.Factory + defp prepare_reacted_post(visibility \\ "private") do + unrelated_user = insert(:user, local: true) + poster = insert(:user, local: true) + follower = insert(:user, local: true) + {:ok, _, _, %{data: %{"state" => "accept"}}} = CommonAPI.follow(poster, follower) + + {:ok, post_activity} = CommonAPI.post(poster, %{status: "miaow!", visibility: visibility}) + + if visibility != "direct" do + assert Visibility.visible_for_user?(post_activity, follower) + end + + if visibility in ["direct", "private"] do + refute Visibility.visible_for_user?(post_activity, unrelated_user) + end + + {:ok, _react_activity} = CommonAPI.react_with_emoji(post_activity.id, follower, "🐾") + + {post_activity, poster, follower, unrelated_user} + end + + defp prepare_conn_of_user(conn, user) do + conn + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write", "read"])) + end + setup do Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) :ok @@ -137,6 +165,28 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do |> json_response_and_validate_schema(400) end + test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji not allowed for non-visible posts", %{ + conn: conn + } do + {%{id: activity_id} = _activity, _author, follower, stranger} = prepare_reacted_post() + + # Works for follower + resp = + prepare_conn_of_user(conn, follower) + |> put("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈") + |> json_response_and_validate_schema(200) + + assert match?(%{"id" => ^activity_id}, resp) + + # Fails for stranger + resp = + prepare_conn_of_user(conn, stranger) + |> put("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈") + |> json_response_and_validate_schema(400) + + assert match?(%{"error" => _}, resp) + end + test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do user = insert(:user) other_user = insert(:user) @@ -211,6 +261,26 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do |> json_response(400) end + test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji only allows original reacter to revoke", + %{conn: conn} do + {%{id: activity_id} = _activity, author, follower, unrelated} = prepare_reacted_post("public") + + # Works for original reacter + prepare_conn_of_user(conn, follower) + |> delete("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐾") + |> json_response_and_validate_schema(200) + + # Fails for anyone else + for u <- [author, unrelated] do + resp = + prepare_conn_of_user(conn, u) + |> delete("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐾") + |> json_response(400) + + assert match?(%{"error" => _}, resp) + end + end + test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do user = insert(:user) other_user = insert(:user) @@ -324,6 +394,28 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do assert [%{"name" => "🎅", "count" => 2}] = result end + test "GET /api/v1/pleroma/statuses/:id/reactions not allowed for non-visible posts", %{ + conn: conn + } do + {%{id: activity_id} = _activity, _author, follower, stranger} = prepare_reacted_post() + + # Works for follower + resp = + prepare_conn_of_user(conn, follower) + |> get("/api/v1/pleroma/statuses/#{activity_id}/reactions") + |> json_response_and_validate_schema(200) + + assert match?([%{"name" => _, "count" => _} | _], resp) + + # Fails for stranger + resp = + prepare_conn_of_user(conn, stranger) + |> get("/api/v1/pleroma/statuses/#{activity_id}/reactions") + |> json_response_and_validate_schema(403) + + assert match?(%{"error" => _}, resp) + end + test "GET /api/v1/pleroma/statuses/:id/reactions with :show_reactions disabled", %{conn: conn} do clear_config([:instance, :show_reactions], false) @@ -372,4 +464,20 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do assert represented_user["id"] == other_user.id end + + test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji not allowed for non-visible posts", %{ + conn: conn + } do + {%{id: activity_id} = _activity, _author, follower, stranger} = prepare_reacted_post() + + # Works for follower + prepare_conn_of_user(conn, follower) + |> get("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈") + |> json_response_and_validate_schema(200) + + # Fails for stranger + prepare_conn_of_user(conn, stranger) + |> get("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈") + |> json_response_and_validate_schema(403) + end end