MastoAPI/CommonAPI: Return 404 when post not visible to user

Akkoma patches returned 403 and some of my previous commits returned 422.
This unifies the errors returned to 404 "Record not found", gaslighting
user just like we do for other endpoints and how Mastodon does it.
This commit is contained in:
Phantasm 2025-12-10 14:31:22 +01:00
commit 293628fb24
No known key found for this signature in database
GPG key ID: 2669E588BCC634C8
5 changed files with 66 additions and 50 deletions

View file

@ -178,6 +178,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
parameters: [id_param()], parameters: [id_param()],
responses: %{ responses: %{
200 => status_response(), 200 => status_response(),
400 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError) 404 => Operation.response("Not Found", "application/json", ApiError)
} }
} }
@ -246,7 +247,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
404 => 404 =>
Operation.response("Not found", "application/json", %Schema{ Operation.response("Not found", "application/json", %Schema{
allOf: [ApiError], allOf: [ApiError],
title: "Unprocessable Entity", title: "Not Found",
example: %{ example: %{
"error" => "Record not found" "error" => "Record not found"
} }
@ -340,7 +341,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
400 => Operation.response("Error", "application/json", ApiError), 400 => Operation.response("Error", "application/json", ApiError),
404 => 404 =>
Operation.response( Operation.response(
"Unprocessable Entity", "Not Found",
"application/json", "application/json",
%Schema{ %Schema{
allOf: [ApiError], allOf: [ApiError],
@ -366,7 +367,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
400 => Operation.response("Error", "application/json", ApiError), 400 => Operation.response("Error", "application/json", ApiError),
404 => 404 =>
Operation.response( Operation.response(
"Error", "Not Found",
"application/json", "application/json",
%Schema{ %Schema{
allOf: [ApiError], allOf: [ApiError],

View file

@ -258,7 +258,7 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, _} = res -> {:ok, _} = res ->
res res
{:error, reason} = res when reason in [:not_found, :forbidden] -> {:error, :not_found} = res ->
res res
{:error, e} -> {:error, e} ->
@ -280,7 +280,7 @@ defmodule Pleroma.Web.CommonAPI do
{:error, :not_found} {:error, :not_found}
{:visible, _} -> {:visible, _} ->
{:error, :forbidden} {:error, :not_found}
{: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
@ -539,6 +539,14 @@ defmodule Pleroma.Web.CommonAPI do
defp activity_belongs_to_actor(%{actor: actor}, actor), do: true defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error} defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
defp activity_visible_to_actor(activity, %User{} = user) do
if Visibility.visible_for_user?(activity, user) do
true
else
{:error, :visibility_error}
end
end
defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
with false <- type in ["Note", "Article", "Question"] do with false <- type in ["Note", "Article", "Question"] do
{:error, :not_allowed} {:error, :not_allowed}
@ -553,7 +561,11 @@ defmodule Pleroma.Web.CommonAPI do
@spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors() @spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
def unpin(id, user) do def unpin(id, user) do
# Order of visibility/belonging matters for MastoAPI responses.
# post not visible -> 404
# post visible, not owned -> 422
with %Activity{} = activity <- create_activity_by_id(id), with %Activity{} = activity <- create_activity_by_id(id),
true <- activity_visible_to_actor(activity, user),
true <- activity_belongs_to_actor(activity, user.ap_id), true <- activity_belongs_to_actor(activity, user.ap_id),
{:ok, unpin_data, _} <- Builder.unpin(user, activity.object), {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
{:ok, _unpin, _} <- {:ok, _unpin, _} <-

View file

@ -154,7 +154,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
add_error(draft, dgettext("errors", "Cannot reply to a deleted status")) add_error(draft, dgettext("errors", "Cannot reply to a deleted status"))
false -> false ->
add_error(draft, dgettext("errors", "Replying to a status that is not visibile to user")) add_error(draft, dgettext("errors", "Record not found"))
{:type, type} -> {:type, type} ->
add_error( add_error(

View file

@ -417,6 +417,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity) try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else else
# Order matters, if status is not owned by user and is not visible to user
# return 404 just like other endpoints
{:error, :visibility_error} ->
{:error, :not_found, "Record not found"}
{:error, :ownership_error} -> {:error, :ownership_error} ->
{:error, :unprocessable_entity, "Someone else's status cannot be unpinned"} {:error, :unprocessable_entity, "Someone else's status cannot be unpinned"}

View file

@ -1597,22 +1597,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
{:ok, activity} = {:ok, activity} =
CommonAPI.post(stranger, %{status: "it can eternal lie", visibility: "private"}) CommonAPI.post(stranger, %{status: "it can eternal lie", visibility: "private"})
resp = assert conn
conn |> put_req_header("content-type", "application/json")
|> put_req_header("content-type", "application/json") |> post("/api/v1/statuses/#{activity.id}/favourite")
|> post("/api/v1/statuses/#{activity.id}/favourite") |> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
|> json_response_and_validate_schema(403)
assert match?(%{"error" => _}, resp)
end end
test "returns 404 error for a wrong id", %{conn: conn} do test "returns 404 error for a wrong id", %{conn: conn} do
conn = assert conn
conn |> put_req_header("content-type", "application/json")
|> put_req_header("content-type", "application/json") |> post("/api/v1/statuses/1/favourite")
|> post("/api/v1/statuses/1/favourite") |> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end end
end end
@ -1639,13 +1634,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
activity = insert(:note_activity) activity = insert(:note_activity)
# using base post ID # using base post ID
resp = assert conn
conn |> put_req_header("content-type", "application/json")
|> put_req_header("content-type", "application/json") |> post("/api/v1/statuses/#{activity.id}/unfavourite")
|> post("/api/v1/statuses/#{activity.id}/unfavourite") |> json_response_and_validate_schema(400) == %{"error" => "Could not unfavorite"}
|> json_response(400)
assert match?(%{"error" => "Could not unfavorite"}, resp)
end end
test "can't unfavourite other user's favs", %{conn: conn} do test "can't unfavourite other user's favs", %{conn: conn} do
@ -1655,13 +1647,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
{:ok, _} = CommonAPI.favorite(activity.id, other) {:ok, _} = CommonAPI.favorite(activity.id, other)
# using base post ID # using base post ID
resp = assert conn
conn |> put_req_header("content-type", "application/json")
|> put_req_header("content-type", "application/json") |> post("/api/v1/statuses/#{activity.id}/unfavourite")
|> post("/api/v1/statuses/#{activity.id}/unfavourite") |> json_response_and_validate_schema(400) == %{"error" => "Could not unfavorite"}
|> json_response(400)
assert match?(%{"error" => "Could not unfavorite"}, resp)
end end
test "can't unfavourite other user's favs using their activity", %{conn: conn} do test "can't unfavourite other user's favs using their activity", %{conn: conn} do
@ -1670,13 +1659,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
other = insert(:user) other = insert(:user)
{:ok, fav_activity} = CommonAPI.favorite(activity.id, other) {: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 # some APIs (used to) take IDs of any activity type, make sure this fails one way or another
resp = assert conn
conn |> put_req_header("content-type", "application/json")
|> put_req_header("content-type", "application/json") |> post("/api/v1/statuses/#{fav_activity.id}/unfavourite")
|> post("/api/v1/statuses/#{fav_activity.id}/unfavourite") |> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
|> json_response_and_validate_schema(404)
assert match?(%{"error" => _}, resp)
end end
test "returns 404 error for a wrong id", %{conn: conn} do test "returns 404 error for a wrong id", %{conn: conn} do
@ -1722,7 +1708,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> json_response(403) == %{"error" => "Invalid credentials."} |> json_response(403) == %{"error" => "Invalid credentials."}
end end
test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do test "/pin: returns 422 error when activity is not public", %{conn: conn, user: user} do
{:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"}) {:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"})
conn = conn =
@ -1769,8 +1755,23 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"} |> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end end
test "/unpin: returns 404 error when activity not visible to user", %{user: user} do
%{conn: conn, user: stranger} = oauth_access(["write:accounts"])
{:ok, activity} = CommonAPI.post(user, %{status: "yumi", visibility: "private"})
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(activity, stranger)
assert conn
|> assign(:user, stranger)
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unpin")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "/unpin: returns 422 error when activity not owned by user", %{activity: activity} do test "/unpin: returns 422 error when activity not owned by user", %{activity: activity} do
%{conn: conn} = oauth_access(["write:accounts"]) %{conn: conn, user: user} = oauth_access(["write:accounts"])
assert Pleroma.Web.ActivityPub.Visibility.visible_for_user?(activity, user)
assert conn assert conn
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
@ -2169,6 +2170,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
test "fails when base post not visible to current user", %{user: user} do test "fails when base post not visible to current user", %{user: user} do
other_user = insert(:user, local: true) other_user = insert(:user, local: true)
%{conn: conn} = oauth_access(["read:accounts"])
{:ok, activity} = {:ok, activity} =
CommonAPI.post(user, %{ CommonAPI.post(user, %{
@ -2176,14 +2178,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
visibility: "private" visibility: "private"
}) })
resp = assert conn
build_conn() |> assign(:user, other_user)
|> assign(:user, other_user) |> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) |> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(404)
assert match?(%{"error" => _}, resp)
end 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