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.
This commit is contained in:
Oneric 2025-12-01 00:06:13 +01:00 committed by Phantasm
commit 59fcb5c96e
No known key found for this signature in database
GPG key ID: 2669E588BCC634C8
11 changed files with 398 additions and 20 deletions

View file

@ -35,7 +35,8 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
security: [%{"oAuth" => ["read:statuses"]}], security: [%{"oAuth" => ["read:statuses"]}],
operationId: "EmojiReactionController.index", operationId: "EmojiReactionController.index",
responses: %{ responses: %{
200 => array_of_reactions_response() 200 => array_of_reactions_response(),
403 => Operation.response("Access denied", "application/json", ApiError)
} }
} }
end end

View file

@ -162,6 +162,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
parameters: [id_param()], parameters: [id_param()],
responses: %{ responses: %{
200 => status_response(), 200 => status_response(),
403 => Operation.response("Access denied", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError) 404 => Operation.response("Not Found", "application/json", ApiError)
} }
} }
@ -388,6 +389,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
"application/json", "application/json",
AccountOperation.array_of_accounts() AccountOperation.array_of_accounts()
), ),
403 => Operation.response("Access denied", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError) 404 => Operation.response("Not Found", "application/json", ApiError)
} }
} }

View file

@ -258,7 +258,7 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, _} = res -> {:ok, _} = res ->
res res
{:error, :not_found} = res -> {:error, reason} = res when reason in [:not_found, :forbidden] ->
res res
{:error, e} -> {:error, e} ->
@ -269,6 +269,7 @@ defmodule Pleroma.Web.CommonAPI do
defp favorite_helper(user, id) do defp favorite_helper(user, id) do
with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)}, 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, like_object, meta}} <- {:build_object, Builder.like(user, object)},
{_, {:ok, %Activity{} = activity, _meta}} <- {_, {:ok, %Activity{} = activity, _meta}} <-
{:common_pipeline, {:common_pipeline,
@ -278,6 +279,9 @@ defmodule Pleroma.Web.CommonAPI do
{:find_object, _} -> {:find_object, _} ->
{:error, :not_found} {:error, :not_found}
{:visible, _} ->
{:error, :forbidden}
{:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e -> {:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
if {:object, {"already liked by this actor", []}} in changeset.errors do if {:object, {"already liked by this actor", []}} in changeset.errors do
{:ok, :already_liked} {:ok, :already_liked}
@ -311,11 +315,15 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, Activity.t()} | {:error, String.t()} {:ok, Activity.t()} | {:error, String.t()}
def react_with_emoji(id, user, emoji) do def react_with_emoji(id, user, emoji) do
with %Activity{} = activity <- Activity.get_by_id(id), with %Activity{} = activity <- Activity.get_by_id(id),
{_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
object <- Object.normalize(activity, fetch: false), object <- Object.normalize(activity, fetch: false),
{:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji), {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
{:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
{:ok, activity} {:ok, activity}
else else
{:visible, _} ->
{:error, dgettext("errors", "Must be able to access post to interact with it")}
_ -> _ ->
{:error, dgettext("errors", "Could not add reaction emoji")} {:error, dgettext("errors", "Could not add reaction emoji")}
end end

View file

@ -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: ""}} = draft), do: draft
defp in_reply_to(%{params: %{in_reply_to_status_id: :deleted}} = draft) do defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do
add_error(draft, dgettext("errors", "Cannot reply to a deleted status")) # If a post was deleted all its activities (except the newly added Delete) are purged too,
end # 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 false ->
activity = Activity.get_by_id(id) add_error(draft, dgettext("errors", "Replying to a status that is not visibile to user"))
params = {:type, type} ->
if is_nil(activity) do add_error(draft, dgettext("errors", "Can only reply to posts, not %{type} activities",
# Deleted activities are returned as nil type: inspect(type)))
Map.put(params, :in_reply_to_status_id, :deleted) end
else
Map.put(params, :in_reply_to_status_id, activity)
end
in_reply_to(%{draft | params: params})
end end
defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do

View file

@ -319,6 +319,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
@doc "DELETE /api/v1/statuses/:id" @doc "DELETE /api/v1/statuses/:id"
def delete(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do def delete(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id), 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 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
try_render(conn, "show.json", try_render(conn, "show.json",
activity: activity, activity: activity,
@ -340,6 +341,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
_ _
) do ) do
with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params), 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 %Activity{} = announce <- Activity.normalize(announce.data) do
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity}) try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
end end
@ -364,6 +366,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
_ _
) do ) do
with {:ok, _fav} <- CommonAPI.favorite(activity_id, user), 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 %Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity) try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end end

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Plugs.OAuthScopesPlug 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 def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
with true <- Pleroma.Config.get([:instance, :show_reactions]), with true <- Pleroma.Config.get([:instance, :show_reactions]),
%Activity{} = activity <- Activity.get_by_id_with_object(activity_id), %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), %Object{} = object <- Object.normalize(activity, fetch: false),
reactions <- Object.get_emoji_reactions(object) do reactions <- Object.get_emoji_reactions(object) do
reactions = reactions =
@ -37,6 +39,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
render(conn, "index.json", emoji_reactions: reactions, user: user) render(conn, "index.json", emoji_reactions: reactions, user: user)
else else
{:visible, _} -> {:error, :forbidden}
_e -> json(conn, []) _e -> json(conn, [])
end end
end end
@ -76,6 +79,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
|> Pleroma.Emoji.fully_qualify_emoji() |> Pleroma.Emoji.fully_qualify_emoji()
|> Pleroma.Emoji.maybe_quote() |> Pleroma.Emoji.maybe_quote()
# CommonAPI checks if allowed to react
with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do
activity = Activity.get_by_id(activity_id) activity = Activity.get_by_id(activity_id)
@ -91,6 +95,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
|> Pleroma.Emoji.fully_qualify_emoji() |> Pleroma.Emoji.fully_qualify_emoji()
|> Pleroma.Emoji.maybe_quote() |> Pleroma.Emoji.maybe_quote()
# CommonAPI checks only author can revoke reactions
with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do
activity = Activity.get_by_id(activity_id) activity = Activity.get_by_id(activity_id)

View file

@ -332,7 +332,7 @@ defmodule Pleroma.Conversation.ParticipationTest do
# When it's a reply from the blocked user # When it's a reply from the blocked user
{:ok, _direct2} = {:ok, _direct2} =
CommonAPI.post(blocked, %{ CommonAPI.post(blocked, %{
status: "reply", status: "@#{third_user.nickname}, #{blocker.nickname} reply",
visibility: "direct", visibility: "direct",
in_reply_to_conversation_id: blocked_participation.id in_reply_to_conversation_id: blocked_participation.id
}) })

View file

@ -66,8 +66,10 @@ defmodule Pleroma.ConversationTest do
jafnhar = insert(:user, local: false) jafnhar = insert(:user, local: false)
tridi = insert(:user) tridi = insert(:user)
to = [har.nickname, jafnhar.nickname, tridi.nickname]
{:ok, activity} = {: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) object = Pleroma.Object.normalize(activity, fetch: false)
context = object.data["context"] context = object.data["context"]
@ -88,7 +90,8 @@ defmodule Pleroma.ConversationTest do
CommonAPI.post(jafnhar, %{ CommonAPI.post(jafnhar, %{
status: "Hey @#{har.nickname}", status: "Hey @#{har.nickname}",
visibility: "direct", 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) object = Pleroma.Object.normalize(activity, fetch: false)
@ -112,7 +115,8 @@ defmodule Pleroma.ConversationTest do
CommonAPI.post(tridi, %{ CommonAPI.post(tridi, %{
status: "Hey @#{har.nickname}", status: "Hey @#{har.nickname}",
visibility: "direct", 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) object = Pleroma.Object.normalize(activity, fetch: false)

View file

@ -316,6 +316,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
user = insert(:user) user = insert(:user)
%{user: other_user, conn: conn} = oauth_access(["read:notifications"]) %{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, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"})
{:ok, direct_activity} = {:ok, direct_activity} =

View file

@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Workers.ScheduledActivityWorker alias Pleroma.Workers.ScheduledActivityWorker
@ -267,6 +268,72 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end) end)
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 test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
conn = conn =
conn conn
@ -1416,6 +1483,24 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert to_string(activity.id) == id assert to_string(activity.id) == id
end 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 end
describe "unreblogging" do 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"} assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end 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 end
describe "favoriting" do describe "favoriting" do
@ -1477,6 +1589,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
end 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 test "returns 404 error for a wrong id", %{conn: conn} do
conn = conn =
conn conn
@ -1506,6 +1633,50 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert to_string(activity.id) == id assert to_string(activity.id) == id
end 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 test "returns 404 error for a wrong id", %{conn: conn} do
conn = conn =
conn conn
@ -1959,6 +2130,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert id == other_user.id assert id == other_user.id
end 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 test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do
clear_config([:instance, :show_reactions], false) clear_config([:instance, :show_reactions], false)
@ -2077,6 +2267,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert [] == response assert [] == response
end 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 end
test "context" do test "context" do
@ -2099,6 +2308,34 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
} = response } = response
end 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 test "favorites paginate correctly" do
%{user: user, conn: conn} = oauth_access(["read:favourites"]) %{user: user, conn: conn} = oauth_access(["read:favourites"])
other_user = insert(:user) other_user = insert(:user)

View file

@ -9,10 +9,38 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Tests.ObanHelpers alias Pleroma.Tests.ObanHelpers
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
import Pleroma.Factory 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 setup do
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok :ok
@ -137,6 +165,28 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
|> json_response_and_validate_schema(400) |> json_response_and_validate_schema(400)
end 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 test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
@ -211,6 +261,26 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
|> json_response(400) |> json_response(400)
end 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 test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
@ -324,6 +394,28 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
assert [%{"name" => "🎅", "count" => 2}] = result assert [%{"name" => "🎅", "count" => 2}] = result
end 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 test "GET /api/v1/pleroma/statuses/:id/reactions with :show_reactions disabled", %{conn: conn} do
clear_config([:instance, :show_reactions], false) clear_config([:instance, :show_reactions], false)
@ -372,4 +464,20 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
assert represented_user["id"] == other_user.id assert represented_user["id"] == other_user.id
end 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 end