Merge pull request 'ActivityPub: check OAuth scopes on C2S endpoints' (#8015) from lambadalambda/pleroma:fix/ap-c2s-oauth-scopes into develop

Reviewed-on: https://git.pleroma.social/pleroma/pleroma/pulls/8015
This commit is contained in:
lain 2026-10-27 07:08:48 +00:00
commit 90eb5e4015
3 changed files with 147 additions and 0 deletions

View file

@ -0,0 +1 @@
Check OAuth token scopes on the ActivityPub C2S endpoints; posting to the outbox with a token now needs the `write` scope

View file

@ -22,8 +22,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Federator
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
alias Pleroma.Web.Plugs.FederatingPlug
alias Pleroma.Web.Plugs.OAuthScopesPlug
require Logger
@ -46,6 +48,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
)
# Checks scopes only for OAuth tokens (signatures and Basic auth already skip the check).
plug(:check_token_scopes, %{scopes: ["read:accounts"]} when action == :whoami)
plug(:check_token_scopes, %{scopes: ["read:statuses"]} when action == :read_inbox)
plug(:check_token_scopes, %{scopes: ["write"]} when action == :update_outbox)
plug(:check_token_scopes, %{scopes: ["write:media"]} when action == :upload_media)
plug(
:check_token_scopes,
%{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
when action in [:outbox, :object_replies]
)
plug(
:check_token_scopes,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
when action in [:followers, :following]
)
plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
plug(
@ -57,6 +77,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
plug(:log_inbox_metadata when action in [:inbox])
plug(:relay_active? when action in [:relay])
defp check_token_scopes(%{assigns: %{token: %Token{}}} = conn, opts),
do: OAuthScopesPlug.call(conn, opts)
defp check_token_scopes(conn, _), do: conn
defp relay_active?(conn, _) do
if Pleroma.Config.get([:instance, :allow_relay]) do
conn

View file

@ -3104,4 +3104,125 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
obj_id in object_ids
end)
end
describe "OAuth scopes" do
setup do
Mox.stub_with(Pleroma.StaticStubbedConfigMock, Pleroma.Config)
[user: insert(:user)]
end
defp with_token(conn, user, scopes) do
token = insert(:oauth_token, user: user, scopes: scopes)
conn
|> put_req_header("authorization", "Bearer #{token.token}")
|> put_req_header("accept", "application/activity+json")
end
test "GET /api/ap/whoami needs read:accounts", %{conn: conn, user: user} do
assert conn |> with_token(user, ["profile"]) |> get("/api/ap/whoami") |> json_response(403)
assert %{"id" => id} =
conn
|> with_token(user, ["read:accounts"])
|> get("/api/ap/whoami")
|> json_response(200)
assert id == user.ap_id
end
test "GET /users/:nickname/inbox needs read:statuses", %{conn: conn, user: user} do
inbox = "/users/#{user.nickname}/inbox"
assert conn |> with_token(user, ["read:accounts"]) |> get(inbox) |> json_response(403)
assert conn |> with_token(user, ["read:statuses"]) |> get(inbox) |> json_response(200)
end
test "POST /users/:nickname/outbox needs write", %{conn: conn, user: user} do
activity = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"object" => %{
"type" => "Note",
"content" => "scoped",
"to" => "https://www.w3.org/ns/activitystreams#Public",
"cc" => []
}
}
post = fn scopes ->
conn
|> with_token(user, scopes)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", activity)
end
assert post.(["profile", "write:media"]) |> json_response(403)
assert post.(["write"]) |> json_response(201)
end
test "POST /api/ap/upload_media is forbidden without write:media", %{conn: conn, user: user} do
assert conn
|> with_token(user, ["write:statuses"])
|> post("/api/ap/upload_media", %{})
|> json_response(403)
end
# majic's libmagic port is unavailable on local Darwin runs; Linux CI still runs this test.
@tag :skip_darwin
test "POST /api/ap/upload_media works with write:media", %{conn: conn, user: user} do
image = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
assert conn
|> with_token(user, ["write:media"])
|> post("/api/ap/upload_media", %{"file" => image})
|> json_response(201)
end
test "GET /users/:nickname/outbox shows private posts only with read:statuses",
%{conn: conn, user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "secret", visibility: "private"})
outbox = "/users/#{user.nickname}/outbox?page=true"
ids = fn scopes ->
conn
|> with_token(user, scopes)
|> get(outbox)
|> json_response(200)
|> Map.fetch!("orderedItems")
|> Enum.map(& &1["id"])
end
refute activity.data["id"] in ids.(["profile"])
assert activity.data["id"] in ids.(["read:statuses"])
end
test "GET /users/:nickname/followers shows hidden followers only with read:accounts",
%{conn: conn} do
user = insert(:user, hide_followers: true)
User.follow(insert(:user), user)
followers = "/users/#{user.nickname}/followers?page=1"
assert conn |> with_token(user, ["profile"]) |> get(followers) |> response(403)
assert %{"orderedItems" => [_]} =
conn |> with_token(user, ["read:accounts"]) |> get(followers) |> json_response(200)
end
test "GET /users/:nickname/following shows hidden follows only with read:accounts",
%{conn: conn} do
user = insert(:user, hide_follows: true)
User.follow(user, insert(:user))
following = "/users/#{user.nickname}/following?page=1"
assert conn |> with_token(user, ["profile"]) |> get(following) |> response(403)
assert %{"orderedItems" => [_]} =
conn |> with_token(user, ["read:accounts"]) |> get(following) |> json_response(200)
end
end
end