fix(activitypub): check OAuth scopes on C2S endpoints
The C2S routes and the outbox/followers/following/replies collections accepted a token with any scope. Requests with an OAuth token now need the matching scope; HTTP signatures and Basic auth are not changed.
This commit is contained in:
parent
9edce3a0a6
commit
da7e75ca73
3 changed files with 147 additions and 0 deletions
1
changelog.d/ap-c2s-oauth-scopes.security
Normal file
1
changelog.d/ap-c2s-oauth-scopes.security
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue