[#1427] Reworked admin scopes support.

Requalified users.is_admin flag as legacy accessor to admin actions in case token lacks admin scope(s).
This commit is contained in:
Ivan Tashkinov 2019-12-06 00:25:44 +03:00
parent 51111e286b
commit af42c00cff
11 changed files with 82 additions and 30 deletions

View File

@ -44,6 +44,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mix task to re-count statuses for all users (`mix pleroma.count_statuses`) - Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache). - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
- MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers. - MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers.
- OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>

View File

@ -560,7 +560,10 @@
base_path: "/oauth", base_path: "/oauth",
providers: ueberauth_providers providers: ueberauth_providers
config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies config :pleroma,
:auth,
enforce_oauth_admin_scope_usage: false,
oauth_consumer_strategies: oauth_consumer_strategies
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false

View File

@ -2094,6 +2094,15 @@
type: :group, type: :group,
description: "Authentication / authorization settings", description: "Authentication / authorization settings",
children: [ children: [
%{
key: :enforce_oauth_admin_scope_usage,
type: :boolean,
description:
"OAuth admin scope requirement toggle. " <>
"If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token " <>
"(client app must support admin scopes). If `false` and token doesn't have admin scope(s)," <>
"`is_admin` user flag grants access to admin-specific actions."
},
%{ %{
key: :auth_template, key: :auth_template,
type: :string, type: :string,

View File

@ -8,7 +8,6 @@ defmodule Mix.Tasks.Pleroma.User do
alias Ecto.Changeset alias Ecto.Changeset
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.OAuth
@shortdoc "Manages Pleroma users" @shortdoc "Manages Pleroma users"
@moduledoc File.read!("docs/administration/CLI_tasks/user.md") @moduledoc File.read!("docs/administration/CLI_tasks/user.md")
@ -354,8 +353,7 @@ def run(["sign_out", nickname]) do
start_pleroma() start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
OAuth.Token.delete_user_tokens(user) User.global_sign_out(user)
OAuth.Authorization.delete_user_authorizations(user)
shell_info("#{nickname} signed out from all apps.") shell_info("#{nickname} signed out from all apps.")
else else
@ -375,10 +373,7 @@ defp set_moderator(user, value) do
end end
defp set_admin(user, value) do defp set_admin(user, value) do
{:ok, user} = {:ok, user} = User.admin_api_update(user, %{is_admin: value})
user
|> Changeset.change(%{is_admin: value})
|> User.update_and_set_cache()
shell_info("Admin status of #{user.nickname}: #{user.is_admin}") shell_info("Admin status of #{user.nickname}: #{user.is_admin}")
user user

View File

@ -65,4 +65,11 @@ def delete(key) do
def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], []) def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], [])
def oauth_consumer_enabled?, do: oauth_consumer_strategies() != [] def oauth_consumer_enabled?, do: oauth_consumer_strategies() != []
def enforce_oauth_admin_scope_usage?, do: !!get([:auth, :enforce_oauth_admin_scope_usage])
def oauth_admin_scopes(scope) do
["admin:#{scope}"] ++
if enforce_oauth_admin_scope_usage?(), do: [], else: [scope]
end
end end

View File

@ -5,19 +5,39 @@
defmodule Pleroma.Plugs.UserIsAdminPlug do defmodule Pleroma.Plugs.UserIsAdminPlug do
import Pleroma.Web.TranslationHelpers import Pleroma.Web.TranslationHelpers
import Plug.Conn import Plug.Conn
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.OAuth
def init(options) do def init(options) do
options options
end end
def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do def call(%Plug.Conn{assigns: %{token: %OAuth.Token{scopes: oauth_scopes} = _token}} = conn, _) do
if OAuth.Scopes.contains_admin_scopes?(oauth_scopes) do
# Note: checking for _any_ admin scope presence, not necessarily fitting requested action.
# Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements.
conn conn
else
fail(conn)
end
end
unless Pleroma.Config.enforce_oauth_admin_scope_usage?() do
# To do: once AdminFE makes use of "admin" scope, disable the following func definition
# (fail on no admin scope(s) in token even if `is_admin` is true)
def call(%Plug.Conn{assigns: %{user: %User{is_admin: true}}} = conn, _) do
conn
end
end end
def call(conn, _) do def call(conn, _) do
fail(conn)
end
defp fail(conn) do
conn conn
|> render_error(:forbidden, "User is not admin.") |> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.")
|> halt |> halt()
end end
end end

View File

@ -1732,13 +1732,27 @@ defp truncate_field(%{"name" => name, "value" => value}) do
end end
def admin_api_update(user, params) do def admin_api_update(user, params) do
user changeset =
|> cast(params, [ cast(user, params, [
:is_moderator, :is_moderator,
:is_admin, :is_admin,
:show_role :show_role
]) ])
|> update_and_set_cache()
with {:ok, updated_user} <- update_and_set_cache(changeset) do
if user.is_admin && !updated_user.is_admin do
# Tokens & authorizations containing any admin scopes must be revoked (revoking all)
global_sign_out(user)
end
{:ok, updated_user}
end
end
@doc "Signs user out of all applications"
def global_sign_out(user) do
OAuth.Authorization.delete_user_authorizations(user)
OAuth.Token.delete_user_tokens(user)
end end
def mascot_update(user, url) do def mascot_update(user, url) do

View File

@ -30,13 +30,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["admin:read:accounts", "read:accounts"]} %{scopes: Pleroma.Config.oauth_admin_scopes("read:accounts")}
when action in [:list_users, :user_show, :right_get, :invites] when action in [:list_users, :user_show, :right_get, :invites]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["admin:write:accounts", "write:accounts"]} %{scopes: Pleroma.Config.oauth_admin_scopes("write:accounts")}
when action in [ when action in [
:get_invite_token, :get_invite_token,
:revoke_invite, :revoke_invite,
@ -58,35 +58,37 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["admin:read:reports", "read:reports"]} when action in [:list_reports, :report_show] %{scopes: Pleroma.Config.oauth_admin_scopes("read:reports")}
when action in [:list_reports, :report_show]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["admin:reports", "write:reports"]} %{scopes: Pleroma.Config.oauth_admin_scopes("write:reports")}
when action in [:report_update_state, :report_respond] when action in [:report_update_state, :report_respond]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["admin:read:statuses", "read:statuses"]} when action == :list_user_statuses %{scopes: Pleroma.Config.oauth_admin_scopes("read:statuses")}
when action == :list_user_statuses
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["admin:write:statuses", "write:statuses"]} %{scopes: Pleroma.Config.oauth_admin_scopes("write:statuses")}
when action in [:status_update, :status_delete] when action in [:status_update, :status_delete]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["admin:read", "read"]} %{scopes: Pleroma.Config.oauth_admin_scopes("read")}
when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log] when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["admin:write", "write"]} %{scopes: Pleroma.Config.oauth_admin_scopes("write")}
when action in [:relay_follow, :relay_unfollow, :config_update] when action in [:relay_follow, :relay_unfollow, :config_update]
) )

View File

@ -83,7 +83,7 @@ defp authorize_admin_scopes(scopes, app_scopes, %User{} = user) do
end end
end end
defp contains_admin_scopes?(scopes) do def contains_admin_scopes?(scopes) do
scopes scopes
|> OAuthScopesPlug.filter_descendants(["admin"]) |> OAuthScopesPlug.filter_descendants(["admin"])
|> Enum.any?() |> Enum.any?()

View File

@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["admin:write", "write"]} %{scopes: Pleroma.Config.oauth_admin_scopes("write")}
when action in [ when action in [
:create, :create,
:delete, :delete,

View File

@ -1537,7 +1537,8 @@ test "returns 403 when requested by a non-admin" do
|> assign(:user, user) |> assign(:user, user)
|> get("/api/pleroma/admin/reports") |> get("/api/pleroma/admin/reports")
assert json_response(conn, :forbidden) == %{"error" => "User is not admin."} assert json_response(conn, :forbidden) ==
%{"error" => "User is not an admin or OAuth admin scope is not granted."}
end end
test "returns 403 when requested by anonymous" do test "returns 403 when requested by anonymous" do