[#1234] Mastodon 2.4.3 hierarchical scopes initial support (WIP).
This commit is contained in:
parent
c45013df8e
commit
b63faf9819
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,12 +207,8 @@ 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)
|
||||||
|
|
||||||
scope [] do
|
|
||||||
pipe_through(:oauth_follow)
|
|
||||||
post("/ostatus_subscribe", UtilController, :do_remote_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
|
||||||
pipe_through(:authenticated_api)
|
pipe_through(:authenticated_api)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue