Keep READ endpoints, purge WRITE
This commit is contained in:
parent
2c7e5b2287
commit
1ed975636b
4 changed files with 294 additions and 14 deletions
|
@ -14,10 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
- Issue preventing fetching anything from IPv6-only instances
|
- Issue preventing fetching anything from IPv6-only instances
|
||||||
- Issue allowing post content to leak via opengraph tags despite :restrict\_unauthenticated being set
|
- Issue allowing post content to leak via opengraph tags despite :estrict\_unauthenticated being set
|
||||||
|
|
||||||
## Removed
|
|
||||||
- ActivityPub Client-To-Server routing; `GET` routes are still there, but writes are out.
|
|
||||||
|
|
||||||
## 2024.03
|
## 2024.03
|
||||||
|
|
||||||
|
|
|
@ -9,12 +9,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
alias Pleroma.Delivery
|
alias Pleroma.Delivery
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
||||||
alias Pleroma.Web.ActivityPub.ObjectView
|
alias Pleroma.Web.ActivityPub.ObjectView
|
||||||
alias Pleroma.Web.ActivityPub.Relay
|
alias Pleroma.Web.ActivityPub.Relay
|
||||||
alias Pleroma.Web.ActivityPub.UserView
|
alias Pleroma.Web.ActivityPub.UserView
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
alias Pleroma.Web.ActivityPub.Visibility
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
|
alias Pleroma.Web.ControllerHelper
|
||||||
alias Pleroma.Web.Endpoint
|
alias Pleroma.Web.Endpoint
|
||||||
alias Pleroma.Web.Federator
|
alias Pleroma.Web.Federator
|
||||||
alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
|
alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
|
||||||
|
@ -33,6 +35,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
[unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
|
[unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Note: :following and :followers must be served even without authentication (as via :api)
|
||||||
|
plug(
|
||||||
|
EnsureAuthenticatedPlug
|
||||||
|
when action in [:read_inbox]
|
||||||
|
)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
Pleroma.Web.Plugs.Cache,
|
Pleroma.Web.Plugs.Cache,
|
||||||
[query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
|
[query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
|
||||||
|
@ -148,7 +156,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /relay/following
|
@doc """
|
||||||
|
GET /relay/following
|
||||||
|
"""
|
||||||
def relay_following(conn, _params) do
|
def relay_following(conn, _params) do
|
||||||
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
|
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
|
||||||
conn
|
conn
|
||||||
|
@ -185,7 +195,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /relay/followers
|
@doc """
|
||||||
|
GET /relay/followers
|
||||||
|
"""
|
||||||
def relay_followers(conn, _params) do
|
def relay_followers(conn, _params) do
|
||||||
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
|
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
|
||||||
conn
|
conn
|
||||||
|
@ -222,9 +234,43 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
def outbox(
|
||||||
POST /users/:nickname/inbox
|
%{assigns: %{user: for_user}} = conn,
|
||||||
"""
|
%{"nickname" => nickname, "page" => page?} = params
|
||||||
|
)
|
||||||
|
when page? in [true, "true"] do
|
||||||
|
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||||
|
# "include_poll_votes" is a hack because postgres generates inefficient
|
||||||
|
# queries when filtering by 'Answer', poll votes will be hidden by the
|
||||||
|
# visibility filter in this case anyway
|
||||||
|
params =
|
||||||
|
params
|
||||||
|
|> Map.drop(["nickname", "page"])
|
||||||
|
|> Map.put("include_poll_votes", true)
|
||||||
|
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
|
||||||
|
|
||||||
|
activities = ActivityPub.fetch_user_activities(user, for_user, params)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/activity+json")
|
||||||
|
|> put_view(UserView)
|
||||||
|
|> render("activity_collection_page.json", %{
|
||||||
|
activities: activities,
|
||||||
|
pagination: ControllerHelper.get_pagination_fields(conn, activities),
|
||||||
|
iri: "#{user.ap_id}/outbox"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def outbox(conn, %{"nickname" => nickname}) do
|
||||||
|
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/activity+json")
|
||||||
|
|> put_view(UserView)
|
||||||
|
|> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
|
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
|
||||||
with %User{} = recipient <- User.get_cached_by_nickname(nickname),
|
with %User{} = recipient <- User.get_cached_by_nickname(nickname),
|
||||||
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
|
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
|
||||||
|
@ -271,6 +317,56 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
|> represent_service_actor(conn)
|
|> represent_service_actor(conn)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def read_inbox(
|
||||||
|
%{assigns: %{user: %User{nickname: nickname} = user}} = conn,
|
||||||
|
%{"nickname" => nickname, "page" => page?} = params
|
||||||
|
)
|
||||||
|
when page? in [true, "true"] do
|
||||||
|
params =
|
||||||
|
params
|
||||||
|
|> Map.drop(["nickname", "page"])
|
||||||
|
|> Map.put("blocking_user", user)
|
||||||
|
|> Map.put("user", user)
|
||||||
|
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
|
||||||
|
|
||||||
|
activities =
|
||||||
|
[user.ap_id | User.following(user)]
|
||||||
|
|> ActivityPub.fetch_activities(params)
|
||||||
|
|> Enum.reverse()
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/activity+json")
|
||||||
|
|> put_view(UserView)
|
||||||
|
|> render("activity_collection_page.json", %{
|
||||||
|
activities: activities,
|
||||||
|
pagination: ControllerHelper.get_pagination_fields(conn, activities),
|
||||||
|
iri: "#{user.ap_id}/inbox"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
|
||||||
|
"nickname" => nickname
|
||||||
|
}) do
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/activity+json")
|
||||||
|
|> put_view(UserView)
|
||||||
|
|> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
|
||||||
|
"nickname" => nickname
|
||||||
|
}) do
|
||||||
|
err =
|
||||||
|
dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
|
||||||
|
nickname: nickname,
|
||||||
|
as_nickname: as_nickname
|
||||||
|
)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_status(:forbidden)
|
||||||
|
|> json(err)
|
||||||
|
end
|
||||||
|
|
||||||
defp errors(conn, {:error, :not_found}) do
|
defp errors(conn, {:error, :not_found}) do
|
||||||
conn
|
conn
|
||||||
|> put_status(:not_found)
|
|> put_status(:not_found)
|
||||||
|
@ -292,9 +388,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
conn
|
conn
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
GET /users/:nickname/collections/featured
|
|
||||||
"""
|
|
||||||
def pinned(conn, %{"nickname" => nickname}) do
|
def pinned(conn, %{"nickname" => nickname}) do
|
||||||
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||||
conn
|
conn
|
||||||
|
|
|
@ -797,15 +797,22 @@ defmodule Pleroma.Web.Router do
|
||||||
plug(:after_auth)
|
plug(:after_auth)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scope "/", Pleroma.Web.ActivityPub do
|
||||||
|
pipe_through([:activitypub_client])
|
||||||
|
|
||||||
|
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
|
||||||
|
|
||||||
|
get("/users/:nickname/outbox", ActivityPubController, :outbox)
|
||||||
|
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
|
||||||
|
end
|
||||||
|
|
||||||
scope "/", Pleroma.Web.ActivityPub do
|
scope "/", Pleroma.Web.ActivityPub do
|
||||||
# Note: html format is supported only if static FE is enabled
|
# Note: html format is supported only if static FE is enabled
|
||||||
pipe_through([:accepts_html_json, :static_fe, :activitypub_client])
|
pipe_through([:accepts_html_json, :static_fe, :activitypub_client])
|
||||||
|
|
||||||
# The following two are used in both staticFE and AP S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`:
|
# The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`:
|
||||||
get("/users/:nickname/followers", ActivityPubController, :followers)
|
get("/users/:nickname/followers", ActivityPubController, :followers)
|
||||||
get("/users/:nickname/following", ActivityPubController, :following)
|
get("/users/:nickname/following", ActivityPubController, :following)
|
||||||
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", Pleroma.Web.ActivityPub do
|
scope "/", Pleroma.Web.ActivityPub do
|
||||||
|
|
|
@ -1028,6 +1028,33 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
assert Activity.get_by_ap_id(data["id"])
|
assert Activity.get_by_ap_id(data["id"])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it rejects reads from other users", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, other_user)
|
||||||
|
|> put_req_header("accept", "application/activity+json")
|
||||||
|
|> get("/users/#{user.nickname}/inbox")
|
||||||
|
|
||||||
|
assert json_response(conn, 403)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns a note activity in a collection", %{conn: conn} do
|
||||||
|
note_activity = insert(:direct_note_activity)
|
||||||
|
note_object = Object.normalize(note_activity, fetch: false)
|
||||||
|
user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> put_req_header("accept", "application/activity+json")
|
||||||
|
|> get("/users/#{user.nickname}/inbox?page=true")
|
||||||
|
|
||||||
|
assert response(conn, 200) =~ note_object.data["content"]
|
||||||
|
end
|
||||||
|
|
||||||
test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
|
test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
data = Map.put(data, "bcc", [user.ap_id])
|
data = Map.put(data, "bcc", [user.ap_id])
|
||||||
|
@ -1096,6 +1123,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
refute recipient.follower_address in activity.data["to"]
|
refute recipient.follower_address in activity.data["to"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it requires authentication", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
conn = put_req_header(conn, "accept", "application/activity+json")
|
||||||
|
|
||||||
|
ret_conn = get(conn, "/users/#{user.nickname}/inbox")
|
||||||
|
assert json_response(ret_conn, 403)
|
||||||
|
|
||||||
|
ret_conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> get("/users/#{user.nickname}/inbox")
|
||||||
|
|
||||||
|
assert json_response(ret_conn, 200)
|
||||||
|
end
|
||||||
|
|
||||||
@tag capture_log: true
|
@tag capture_log: true
|
||||||
test "forwarded report", %{conn: conn} do
|
test "forwarded report", %{conn: conn} do
|
||||||
|
@ -1235,6 +1276,148 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "GET /users/:nickname/outbox" do
|
||||||
|
test "it paginates correctly", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
conn = assign(conn, :user, user)
|
||||||
|
outbox_endpoint = user.ap_id <> "/outbox"
|
||||||
|
|
||||||
|
_posts =
|
||||||
|
for i <- 0..25 do
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
|
||||||
|
activity
|
||||||
|
end
|
||||||
|
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> put_req_header("accept", "application/activity+json")
|
||||||
|
|> get(outbox_endpoint <> "?page=true")
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
|
||||||
|
assert length(result["orderedItems"]) == 20
|
||||||
|
assert length(result_ids) == 20
|
||||||
|
assert result["next"]
|
||||||
|
assert String.starts_with?(result["next"], outbox_endpoint)
|
||||||
|
|
||||||
|
result_next =
|
||||||
|
conn
|
||||||
|
|> put_req_header("accept", "application/activity+json")
|
||||||
|
|> get(result["next"])
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end)
|
||||||
|
assert length(result_next["orderedItems"]) == 6
|
||||||
|
assert length(result_next_ids) == 6
|
||||||
|
refute Enum.find(result_next_ids, fn x -> x in result_ids end)
|
||||||
|
refute Enum.find(result_ids, fn x -> x in result_next_ids end)
|
||||||
|
assert String.starts_with?(result["id"], outbox_endpoint)
|
||||||
|
|
||||||
|
result_next_again =
|
||||||
|
conn
|
||||||
|
|> put_req_header("accept", "application/activity+json")
|
||||||
|
|> get(result_next["id"])
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
assert result_next == result_next_again
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns 200 even if there're no activities", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
outbox_endpoint = user.ap_id <> "/outbox"
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> put_req_header("accept", "application/activity+json")
|
||||||
|
|> get(outbox_endpoint)
|
||||||
|
|
||||||
|
result = json_response(conn, 200)
|
||||||
|
assert outbox_endpoint == result["id"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns a local note activity when authenticated as local user", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
reader = insert(:user)
|
||||||
|
{:ok, note_activity} = CommonAPI.post(user, %{status: "mew mew", visibility: "local"})
|
||||||
|
ap_id = note_activity.data["id"]
|
||||||
|
|
||||||
|
resp =
|
||||||
|
conn
|
||||||
|
|> assign(:user, reader)
|
||||||
|
|> put_req_header("accept", "application/activity+json")
|
||||||
|
|> get("/users/#{user.nickname}/outbox?page=true")
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
assert %{"orderedItems" => [%{"id" => ^ap_id}]} = resp
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it does not return a local note activity when unauthenticated", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, _note_activity} = CommonAPI.post(user, %{status: "mew mew", visibility: "local"})
|
||||||
|
|
||||||
|
resp =
|
||||||
|
conn
|
||||||
|
|> put_req_header("accept", "application/activity+json")
|
||||||
|
|> get("/users/#{user.nickname}/outbox?page=true")
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
assert %{"orderedItems" => []} = resp
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns a note activity in a collection", %{conn: conn} do
|
||||||
|
note_activity = insert(:note_activity)
|
||||||
|
note_object = Object.normalize(note_activity, fetch: false)
|
||||||
|
user = User.get_cached_by_ap_id(note_activity.data["actor"])
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> put_req_header("accept", "application/activity+json")
|
||||||
|
|> get("/users/#{user.nickname}/outbox?page=true")
|
||||||
|
|
||||||
|
assert response(conn, 200) =~ note_object.data["content"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns an announce activity in a collection", %{conn: conn} do
|
||||||
|
announce_activity = insert(:announce_activity)
|
||||||
|
user = User.get_cached_by_ap_id(announce_activity.data["actor"])
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> put_req_header("accept", "application/activity+json")
|
||||||
|
|> get("/users/#{user.nickname}/outbox?page=true")
|
||||||
|
|
||||||
|
assert response(conn, 200) =~ announce_activity.data["object"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "It returns poll Answers when authenticated", %{conn: conn} do
|
||||||
|
poller = insert(:user)
|
||||||
|
voter = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(poller, %{
|
||||||
|
status: "suya...",
|
||||||
|
poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert question = Object.normalize(activity, fetch: false)
|
||||||
|
|
||||||
|
{:ok, [activity], _object} = CommonAPI.vote(voter, question, [1])
|
||||||
|
|
||||||
|
assert outbox_get =
|
||||||
|
conn
|
||||||
|
|> assign(:user, voter)
|
||||||
|
|> put_req_header("accept", "application/activity+json")
|
||||||
|
|> get(voter.ap_id <> "/outbox?page=true")
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
assert [answer_outbox] = outbox_get["orderedItems"]
|
||||||
|
assert answer_outbox["id"] == activity.data["id"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "/relay/followers" do
|
describe "/relay/followers" do
|
||||||
test "it returns relay followers", %{conn: conn} do
|
test "it returns relay followers", %{conn: conn} do
|
||||||
relay_actor = Relay.get_actor()
|
relay_actor = Relay.get_actor()
|
||||||
|
|
Loading…
Reference in a new issue