[#1234] Mastodon 2.4.3 hierarchical scopes initial support (WIP).

This commit is contained in:
Ivan Tashkinov 2019-09-08 15:00:03 +03:00
parent c45013df8e
commit b63faf9819
8 changed files with 113 additions and 30 deletions

View file

@ -13,15 +13,16 @@ def init(%{scopes: _} = options), do: options
def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
op = options[:op] || :| op = options[:op] || :|
token = assigns[:token] token = assigns[:token]
matched_scopes = token && filter_descendants(scopes, token.scopes)
cond do cond do
is_nil(token) -> is_nil(token) ->
conn conn
op == :| && scopes -- token.scopes != scopes -> op == :| && Enum.any?(matched_scopes) ->
conn conn
op == :& && scopes -- token.scopes == [] -> op == :& && matched_scopes == scopes ->
conn conn
options[:fallback] == :proceed_unauthenticated -> options[:fallback] == :proceed_unauthenticated ->
@ -30,7 +31,7 @@ def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
|> assign(:token, nil) |> assign(:token, nil)
true -> true ->
missing_scopes = scopes -- token.scopes missing_scopes = scopes -- matched_scopes
permissions = Enum.join(missing_scopes, " #{op} ") permissions = Enum.join(missing_scopes, " #{op} ")
error_message = error_message =
@ -42,4 +43,17 @@ def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
|> halt() |> halt()
end end
end end
@doc "Filters descendants of supported scopes"
def filter_descendants(scopes, supported_scopes) do
Enum.filter(
scopes,
fn scope ->
Enum.find(
supported_scopes,
&(scope == &1 || String.starts_with?(scope, &1 <> ":"))
)
end
)
end
end end

View file

@ -19,6 +19,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Pagination alias Pleroma.Pagination
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.ScheduledActivity alias Pleroma.ScheduledActivity
@ -52,6 +53,41 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
require Logger require Logger
require Pleroma.Constants require Pleroma.Constants
plug(
OAuthScopesPlug,
%{scopes: ["follow", "read:blocks"]} when action in [:blocks, :domain_blocks]
)
plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:blocks"]}
when action in [:block, :unblock, :block_domain, :unblock_domain]
)
plug(OAuthScopesPlug, %{scopes: ["follow", "read:follows"]} when action == :follow_requests)
plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:follows"]}
when action in [
:follow,
:unfollow,
:subscribe,
:unsubscribe,
:authorize_follow_request,
:reject_follow_request
]
)
plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
plug(
OAuthScopesPlug,
%{scopes: ["write:mutes"]}
when action in [:mute_conversation, :unmute_conversation]
)
@rate_limited_relations_actions ~w(follow unfollow)a @rate_limited_relations_actions ~w(follow unfollow)a
@rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status

View file

@ -451,7 +451,7 @@ defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
defp validate_scopes(app, params) do defp validate_scopes(app, params) do
params params
|> Scopes.fetch_scopes(app.scopes) |> Scopes.fetch_scopes(app.scopes)
|> Scopes.validates(app.scopes) |> Scopes.validate(app.scopes)
end end
def default_redirect_uri(%App{} = app) do def default_redirect_uri(%App{} = app) do

View file

@ -8,7 +8,7 @@ defmodule Pleroma.Web.OAuth.Scopes do
""" """
@doc """ @doc """
Fetch scopes from requiest params. Fetch scopes from request params.
Note: `scopes` is used by Mastodon supporting it but sticking to Note: `scopes` is used by Mastodon supporting it but sticking to
OAuth's standard `scope` wherever we control it OAuth's standard `scope` wherever we control it
@ -53,14 +53,14 @@ def to_string(scopes), do: Enum.join(scopes, " ")
@doc """ @doc """
Validates scopes. Validates scopes.
""" """
@spec validates(list() | nil, list()) :: @spec validate(list() | nil, list()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
def validates([], _app_scopes), do: {:error, :missing_scopes} def validate([], _app_scopes), do: {:error, :missing_scopes}
def validates(nil, _app_scopes), do: {:error, :missing_scopes} def validate(nil, _app_scopes), do: {:error, :missing_scopes}
def validates(scopes, app_scopes) do def validate(scopes, app_scopes) do
case scopes -- app_scopes do case Pleroma.Plugs.OAuthScopesPlug.filter_descendants(scopes, app_scopes) do
[] -> {:ok, scopes} ^scopes -> {:ok, scopes}
_ -> {:error, :unsupported_scopes} _ -> {:error, :unsupported_scopes}
end end
end end

View file

@ -104,10 +104,6 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["write"]}) plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["write"]})
end end
pipeline :oauth_follow do
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["follow"]})
end
pipeline :oauth_push do pipeline :oauth_push do
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]}) plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
end end
@ -211,11 +207,7 @@ defmodule Pleroma.Web.Router do
post("/main/ostatus", UtilController, :remote_subscribe) post("/main/ostatus", UtilController, :remote_subscribe)
get("/ostatus_subscribe", UtilController, :remote_follow) get("/ostatus_subscribe", UtilController, :remote_follow)
post("/ostatus_subscribe", UtilController, :do_remote_follow)
scope [] do
pipe_through(:oauth_follow)
post("/ostatus_subscribe", UtilController, :do_remote_follow)
end
end end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do scope "/api/pleroma", Pleroma.Web.TwitterAPI do
@ -231,8 +223,6 @@ defmodule Pleroma.Web.Router do
end end
scope [] do scope [] do
pipe_through(:oauth_follow)
post("/blocks_import", UtilController, :blocks_import) post("/blocks_import", UtilController, :blocks_import)
post("/follow_import", UtilController, :follow_import) post("/follow_import", UtilController, :follow_import)
end end
@ -373,8 +363,6 @@ defmodule Pleroma.Web.Router do
end end
scope [] do scope [] do
pipe_through(:oauth_follow)
post("/follows", MastodonAPIController, :follow) post("/follows", MastodonAPIController, :follow)
post("/accounts/:id/follow", MastodonAPIController, :follow) post("/accounts/:id/follow", MastodonAPIController, :follow)

View file

@ -13,11 +13,20 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Healthcheck alias Pleroma.Healthcheck
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:follows"]}
when action in [:do_remote_follow, :follow_import]
)
plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks_import)
plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version]) plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version])
def help_test(conn, _params) do def help_test(conn, _params) do

View file

@ -84,7 +84,8 @@ test "proceeds with cleared `assigns[:user]` if `token.scopes` doesn't fulfill s
refute conn.assigns[:user] refute conn.assigns[:user]
end end
test "returns 403 and halts in case of no :fallback option and `token.scopes` not fulfilling specified 'any of' conditions", test "returns 403 and halts " <>
"in case of no :fallback option and `token.scopes` not fulfilling specified 'any of' conditions",
%{conn: conn} do %{conn: conn} do
token = insert(:oauth_token, scopes: ["read", "write"]) token = insert(:oauth_token, scopes: ["read", "write"])
any_of_scopes = ["follow"] any_of_scopes = ["follow"]
@ -101,7 +102,8 @@ test "returns 403 and halts in case of no :fallback option and `token.scopes` no
assert Jason.encode!(%{error: expected_error}) == conn.resp_body assert Jason.encode!(%{error: expected_error}) == conn.resp_body
end end
test "returns 403 and halts in case of no :fallback option and `token.scopes` not fulfilling specified 'all of' conditions", test "returns 403 and halts " <>
"in case of no :fallback option and `token.scopes` not fulfilling specified 'all of' conditions",
%{conn: conn} do %{conn: conn} do
token = insert(:oauth_token, scopes: ["read", "write"]) token = insert(:oauth_token, scopes: ["read", "write"])
all_of_scopes = ["write", "follow"] all_of_scopes = ["write", "follow"]
@ -119,4 +121,36 @@ test "returns 403 and halts in case of no :fallback option and `token.scopes` no
assert Jason.encode!(%{error: expected_error}) == conn.resp_body assert Jason.encode!(%{error: expected_error}) == conn.resp_body
end end
describe "with hierarchical scopes, " do
test "proceeds with no op if `token.scopes` fulfill specified 'any of' conditions", %{
conn: conn
} do
token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
conn =
conn
|> assign(:user, token.user)
|> assign(:token, token)
|> OAuthScopesPlug.call(%{scopes: ["read:something"]})
refute conn.halted
assert conn.assigns[:user]
end
test "proceeds with no op if `token.scopes` fulfill specified 'all of' conditions", %{
conn: conn
} do
token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user)
conn =
conn
|> assign(:user, token.user)
|> assign(:token, token)
|> OAuthScopesPlug.call(%{scopes: ["scope1:subscope", "scope2:subscope"], op: :&})
refute conn.halted
assert conn.assigns[:user]
end
end
end end

View file

@ -78,19 +78,21 @@ test "it imports new-style mastodon follow lists", %{conn: conn} do
assert response == "job started" assert response == "job started"
end end
test "requires 'follow' permission", %{conn: conn} do test "requires 'follow' or 'write:follows' permissions", %{conn: conn} do
token1 = insert(:oauth_token, scopes: ["read", "write"]) token1 = insert(:oauth_token, scopes: ["read", "write"])
token2 = insert(:oauth_token, scopes: ["follow"]) token2 = insert(:oauth_token, scopes: ["follow"])
token3 = insert(:oauth_token, scopes: ["something"])
another_user = insert(:user) another_user = insert(:user)
for token <- [token1, token2] do for token <- [token1, token2, token3] do
conn = conn =
conn conn
|> put_req_header("authorization", "Bearer #{token.token}") |> put_req_header("authorization", "Bearer #{token.token}")
|> post("/api/pleroma/follow_import", %{"list" => "#{another_user.ap_id}"}) |> post("/api/pleroma/follow_import", %{"list" => "#{another_user.ap_id}"})
if token == token1 do if token == token3 do
assert %{"error" => "Insufficient permissions: follow."} == json_response(conn, 403) assert %{"error" => "Insufficient permissions: follow | write:follows."} ==
json_response(conn, 403)
else else
assert json_response(conn, 200) assert json_response(conn, 200)
end end