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.ControllerHelper
|
||||||
alias Pleroma.Web.Endpoint
|
alias Pleroma.Web.Endpoint
|
||||||
alias Pleroma.Web.Federator
|
alias Pleroma.Web.Federator
|
||||||
|
alias Pleroma.Web.OAuth.Token
|
||||||
alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
|
alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
|
||||||
alias Pleroma.Web.Plugs.FederatingPlug
|
alias Pleroma.Web.Plugs.FederatingPlug
|
||||||
|
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
|
@ -46,6 +48,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
|
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(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
|
|
@ -57,6 +77,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
plug(:log_inbox_metadata when action in [:inbox])
|
plug(:log_inbox_metadata when action in [:inbox])
|
||||||
plug(:relay_active? when action in [:relay])
|
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
|
defp relay_active?(conn, _) do
|
||||||
if Pleroma.Config.get([:instance, :allow_relay]) do
|
if Pleroma.Config.get([:instance, :allow_relay]) do
|
||||||
conn
|
conn
|
||||||
|
|
|
||||||
|
|
@ -3104,4 +3104,125 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
||||||
obj_id in object_ids
|
obj_id in object_ids
|
||||||
end)
|
end)
|
||||||
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
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue