activity_pub_controller: Add authentication to object & activity endpoints

This commit is contained in:
Haelwenn (lanodan) Monnier 2021-01-21 17:45:42 +01:00
parent fd2477dfba
commit 8c7b3b20d8
No known key found for this signature in database
GPG key ID: D5B7A8E43C997DEE
4 changed files with 168 additions and 23 deletions

View file

@ -79,11 +79,11 @@ def user(conn, %{"nickname" => nickname}) do
end end
end end
def object(conn, _) do def object(%{assigns: assigns} = conn, _) do
with ap_id <- Endpoint.url() <> conn.request_path, with ap_id <- Endpoint.url() <> conn.request_path,
%Object{} = object <- Object.get_cached_by_ap_id(ap_id), %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, Visibility.is_public?(object)}, user <- Map.get(assigns, :user, nil),
{_, false} <- {:local?, Visibility.is_local_public?(object)} do {_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
conn conn
|> assign(:tracking_fun_data, object.id) |> assign(:tracking_fun_data, object.id)
|> set_cache_ttl_for(object) |> set_cache_ttl_for(object)
@ -91,11 +91,8 @@ def object(conn, _) do
|> put_view(ObjectView) |> put_view(ObjectView)
|> render("object.json", object: object) |> render("object.json", object: object)
else else
{:public?, false} -> {:visible?, false} -> {:error, :not_found}
{:error, :not_found} nil -> {:error, :not_found}
{:local?, true} ->
{:error, :not_found}
end end
end end
@ -109,11 +106,12 @@ def track_object_fetch(conn, object_id) do
conn conn
end end
def activity(conn, _params) do def activity(%{assigns: assigns} = conn, _) do
with ap_id <- Endpoint.url() <> conn.request_path, with ap_id <- Endpoint.url() <> conn.request_path,
%Activity{} = activity <- Activity.normalize(ap_id), %Activity{} = activity <- Activity.normalize(ap_id),
{_, true} <- {:public?, Visibility.is_public?(activity)}, {_, true} <- {:local?, activity.local},
{_, false} <- {:local?, Visibility.is_local_public?(activity)} do user <- Map.get(assigns, :user, nil),
{_, true} <- {:visible?, Visibility.visible_for_user?(activity, user)} do
conn conn
|> maybe_set_tracking_data(activity) |> maybe_set_tracking_data(activity)
|> set_cache_ttl_for(activity) |> set_cache_ttl_for(activity)
@ -121,8 +119,8 @@ def activity(conn, _params) do
|> put_view(ObjectView) |> put_view(ObjectView)
|> render("object.json", object: activity) |> render("object.json", object: activity)
else else
{:public?, false} -> {:error, :not_found} {:visible?, false} -> {:error, :not_found}
{:local?, true} -> {:error, :not_found} {:local?, false} -> {:error, :not_found}
nil -> {:error, :not_found} nil -> {:error, :not_found}
end end
end end

View file

@ -56,11 +56,10 @@ def is_direct?(activity) do
def is_list?(%{data: %{"listMessage" => _}}), do: true def is_list?(%{data: %{"listMessage" => _}}), do: true
def is_list?(_), do: false def is_list?(_), do: false
@spec visible_for_user?(Activity.t() | nil, User.t() | nil) :: boolean() @spec visible_for_user?(Object.t() | Activity.t() | nil, User.t() | nil) :: boolean()
def visible_for_user?(%Activity{actor: ap_id}, %User{ap_id: ap_id}), do: true def visible_for_user?(%Activity{actor: ap_id}, %User{ap_id: ap_id}), do: true
def visible_for_user?(%Object{data: %{"actor" => ap_id}}, %User{ap_id: ap_id}), do: true
def visible_for_user?(nil, _), do: false def visible_for_user?(nil, _), do: false
def visible_for_user?(%Activity{data: %{"listMessage" => _}}, nil), do: false def visible_for_user?(%Activity{data: %{"listMessage" => _}}, nil), do: false
def visible_for_user?( def visible_for_user?(
@ -73,16 +72,18 @@ def visible_for_user?(
|> Pleroma.List.member?(user) |> Pleroma.List.member?(user)
end end
def visible_for_user?(%Activity{} = activity, nil) do def visible_for_user?(%{__struct__: module} = message, nil)
if restrict_unauthenticated_access?(activity), when module in [Activity, Object] do
if restrict_unauthenticated_access?(message),
do: false, do: false,
else: is_public?(activity) else: is_public?(message) and not is_local_public?(message)
end end
def visible_for_user?(%Activity{} = activity, user) do def visible_for_user?(%{__struct__: module} = message, user)
when module in [Activity, Object] do
x = [user.ap_id | User.following(user)] x = [user.ap_id | User.following(user)]
y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || []) y = [message.data["actor"]] ++ message.data["to"] ++ (message.data["cc"] || [])
is_public?(activity) || Enum.any?(x, &(&1 in y)) is_public?(message) || Enum.any?(x, &(&1 in y))
end end
def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do

View file

@ -229,6 +229,24 @@ test "it doesn't return a local-only object", %{conn: conn} do
assert json_response(conn, 404) assert json_response(conn, 404)
end end
test "returns local-only objects when authenticated", %{conn: conn} do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
object = Object.normalize(post, fetch: false)
uuid = String.split(object.data["id"], "/") |> List.last()
assert response =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(response, 200) == ObjectView.render("object.json", %{object: object})
end
test "it returns a json representation of the object with accept application/json", %{ test "it returns a json representation of the object with accept application/json", %{
conn: conn conn: conn
} do } do
@ -285,6 +303,28 @@ test "it returns 404 for non-public messages", %{conn: conn} do
assert json_response(conn, 404) assert json_response(conn, 404)
end end
test "returns visible non-public messages when authenticated", %{conn: conn} do
note = insert(:direct_note)
uuid = String.split(note.data["id"], "/") |> List.last()
user = User.get_by_ap_id(note.data["actor"])
marisa = insert(:user)
assert conn
|> assign(:user, marisa)
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
|> json_response(404)
assert response =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
|> json_response(200)
assert response == ObjectView.render("object.json", %{object: note})
end
test "it returns 404 for tombstone objects", %{conn: conn} do test "it returns 404 for tombstone objects", %{conn: conn} do
tombstone = insert(:tombstone) tombstone = insert(:tombstone)
uuid = String.split(tombstone.data["id"], "/") |> List.last() uuid = String.split(tombstone.data["id"], "/") |> List.last()
@ -358,6 +398,23 @@ test "it doesn't return a local-only activity", %{conn: conn} do
assert json_response(conn, 404) assert json_response(conn, 404)
end end
test "returns local-only activities when authenticated", %{conn: conn} do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
uuid = String.split(post.data["id"], "/") |> List.last()
assert response =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(response, 200) == ObjectView.render("object.json", %{object: post})
end
test "it returns a json representation of the activity", %{conn: conn} do test "it returns a json representation of the activity", %{conn: conn} do
activity = insert(:note_activity) activity = insert(:note_activity)
uuid = String.split(activity.data["id"], "/") |> List.last() uuid = String.split(activity.data["id"], "/") |> List.last()
@ -382,6 +439,28 @@ test "it returns 404 for non-public activities", %{conn: conn} do
assert json_response(conn, 404) assert json_response(conn, 404)
end end
test "returns visible non-public messages when authenticated", %{conn: conn} do
note = insert(:direct_note_activity)
uuid = String.split(note.data["id"], "/") |> List.last()
user = User.get_by_ap_id(note.data["actor"])
marisa = insert(:user)
assert conn
|> assign(:user, marisa)
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
|> json_response(404)
assert response =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
|> json_response(200)
assert response == ObjectView.render("object.json", %{object: note})
end
test "it caches a response", %{conn: conn} do test "it caches a response", %{conn: conn} do
activity = insert(:note_activity) activity = insert(:note_activity)
uuid = String.split(activity.data["id"], "/") |> List.last() uuid = String.split(activity.data["id"], "/") |> List.last()

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
use Pleroma.DataCase, async: true use Pleroma.DataCase, async: true
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
import Pleroma.Factory import Pleroma.Factory
@ -107,7 +108,7 @@ test "is_list?", %{
assert Visibility.is_list?(list) assert Visibility.is_list?(list)
end end
test "visible_for_user?", %{ test "visible_for_user? Activity", %{
public: public, public: public,
private: private, private: private,
direct: direct, direct: direct,
@ -149,10 +150,76 @@ test "visible_for_user?", %{
refute Visibility.visible_for_user?(private, unrelated) refute Visibility.visible_for_user?(private, unrelated)
refute Visibility.visible_for_user?(direct, unrelated) refute Visibility.visible_for_user?(direct, unrelated)
# Public and unlisted visible for unauthenticated
assert Visibility.visible_for_user?(public, nil)
assert Visibility.visible_for_user?(unlisted, nil)
refute Visibility.visible_for_user?(private, nil)
refute Visibility.visible_for_user?(direct, nil)
# Visible for a list member # Visible for a list member
assert Visibility.visible_for_user?(list, unrelated) assert Visibility.visible_for_user?(list, unrelated)
end end
test "visible_for_user? Object", %{
public: public,
private: private,
direct: direct,
unlisted: unlisted,
user: user,
mentioned: mentioned,
following: following,
unrelated: unrelated,
list: list
} do
public = Object.normalize(public)
private = Object.normalize(private)
unlisted = Object.normalize(unlisted)
direct = Object.normalize(direct)
list = Object.normalize(list)
# All visible to author
assert Visibility.visible_for_user?(public, user)
assert Visibility.visible_for_user?(private, user)
assert Visibility.visible_for_user?(unlisted, user)
assert Visibility.visible_for_user?(direct, user)
assert Visibility.visible_for_user?(list, user)
# All visible to a mentioned user
assert Visibility.visible_for_user?(public, mentioned)
assert Visibility.visible_for_user?(private, mentioned)
assert Visibility.visible_for_user?(unlisted, mentioned)
assert Visibility.visible_for_user?(direct, mentioned)
assert Visibility.visible_for_user?(list, mentioned)
# DM not visible for just follower
assert Visibility.visible_for_user?(public, following)
assert Visibility.visible_for_user?(private, following)
assert Visibility.visible_for_user?(unlisted, following)
refute Visibility.visible_for_user?(direct, following)
refute Visibility.visible_for_user?(list, following)
# Public and unlisted visible for unrelated user
assert Visibility.visible_for_user?(public, unrelated)
assert Visibility.visible_for_user?(unlisted, unrelated)
refute Visibility.visible_for_user?(private, unrelated)
refute Visibility.visible_for_user?(direct, unrelated)
# Public and unlisted visible for unauthenticated
assert Visibility.visible_for_user?(public, nil)
assert Visibility.visible_for_user?(unlisted, nil)
refute Visibility.visible_for_user?(private, nil)
refute Visibility.visible_for_user?(direct, nil)
# Visible for a list member
# assert Visibility.visible_for_user?(list, unrelated)
end
test "doesn't die when the user doesn't exist", test "doesn't die when the user doesn't exist",
%{ %{
direct: direct, direct: direct,