Merge branch 'authenticated-api-oauth-check-enforcement' into 'develop'

Enforcement of OAuth scopes check for authenticated API endpoints

See merge request pleroma/pleroma!2349
This commit is contained in:
rinpatch 2020-04-16 21:58:57 +00:00
commit da4923f2e5
17 changed files with 248 additions and 40 deletions

View file

@ -0,0 +1,17 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.AuthExpectedPlug do
import Plug.Conn
def init(options), do: options
def call(conn, _) do
put_private(conn, :auth_expected, true)
end
def auth_expected?(conn) do
conn.private[:auth_expected]
end
end

View file

@ -8,12 +8,15 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
alias Pleroma.Config
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.PlugHelper
use Pleroma.Web, :plug
@behaviour Plug
def init(%{scopes: _} = options), do: options
def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
op = options[:op] || :|
token = assigns[:token]

View file

@ -0,0 +1,38 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.PlugHelper do
@moduledoc "Pleroma Plug helper"
def append_to_called_plugs(conn, plug_module) do
append_to_private_list(conn, :called_plugs, plug_module)
end
def append_to_skipped_plugs(conn, plug_module) do
append_to_private_list(conn, :skipped_plugs, plug_module)
end
def plug_called?(conn, plug_module) do
contained_in_private_list?(conn, :called_plugs, plug_module)
end
def plug_skipped?(conn, plug_module) do
contained_in_private_list?(conn, :skipped_plugs, plug_module)
end
def plug_called_or_skipped?(conn, plug_module) do
plug_called?(conn, plug_module) || plug_skipped?(conn, plug_module)
end
defp append_to_private_list(conn, private_variable, value) do
list = conn.private[private_variable] || []
modified_list = Enum.uniq(list ++ [value])
Plug.Conn.put_private(conn, private_variable, modified_list)
end
defp contained_in_private_list?(conn, private_variable, value) do
list = conn.private[private_variable] || []
value in list
end
end

View file

@ -0,0 +1,31 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# A test controller reachable only in :test env.
# Serves to test OAuth scopes check skipping / enforcement.
defmodule Pleroma.Tests.OAuthTestController do
@moduledoc false
use Pleroma.Web, :controller
alias Pleroma.Plugs.OAuthScopesPlug
plug(:skip_plug, OAuthScopesPlug when action == :skipped_oauth)
plug(OAuthScopesPlug, %{scopes: ["read"]} when action != :missed_oauth)
def skipped_oauth(conn, _params) do
noop(conn)
end
def performed_oauth(conn, _params) do
noop(conn)
end
def missed_oauth(conn, _params) do
noop(conn)
end
defp noop(conn), do: json(conn, %{})
end

View file

@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastoFEController do
when action == :index
)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :manifest])
@doc "GET /web/*path"
def index(%{assigns: %{user: user, token: token}} = conn, _params)

View file

@ -15,10 +15,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonAPIController
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI
plug(:skip_plug, OAuthScopesPlug when action == :identity_proofs)
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
@ -366,6 +369,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/endorsements"
def endorsements(conn, params),
do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
@doc "GET /api/v1/identity_proofs"
def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
end

View file

@ -3,21 +3,31 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
@moduledoc """
Contains stubs for unimplemented Mastodon API endpoints.
Note: instead of routing directly to this controller's action,
it's preferable to define an action in relevant (non-generic) controller,
set up OAuth rules for it and call this controller's function from it.
"""
use Pleroma.Web, :controller
require Logger
plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug when action in [:empty_array, :empty_object])
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
# Stubs for unimplemented mastodon api
#
def empty_array(conn, _) do
Logger.debug("Unimplemented, returning an empty array")
Logger.debug("Unimplemented, returning an empty array (list)")
json(conn, [])
end
def empty_object(conn, _) do
Logger.debug("Unimplemented, returning an empty object")
Logger.debug("Unimplemented, returning an empty object (map)")
json(conn, %{})
end
end

View file

@ -5,10 +5,13 @@
defmodule Pleroma.Web.MastodonAPI.SuggestionController do
use Pleroma.Web, :controller
alias Pleroma.Plugs.OAuthScopesPlug
require Logger
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
@doc "GET /api/v1/suggestions"
def index(conn, _) do
json(conn, [])
end
def index(conn, params),
do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
end

View file

@ -27,6 +27,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
plug(:fetch_flash)
plug(RateLimiter, [name: :authentication] when action == :create_authorization)
plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug)
action_fallback(Pleroma.Web.OAuth.FallbackController)
@oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"

View file

@ -34,7 +34,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
plug(
OAuthScopesPlug,
%{scopes: ["write:conversations"]} when action == :update_conversation
%{scopes: ["write:conversations"]} when action in [:update_conversation, :read_conversations]
)
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification)

View file

@ -34,6 +34,7 @@ defmodule Pleroma.Web.Router do
pipeline :authenticated_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.AuthExpectedPlug)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
@ -334,7 +335,7 @@ defmodule Pleroma.Web.Router do
get("/accounts/relationships", AccountController, :relationships)
get("/accounts/:id/lists", AccountController, :lists)
get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
get("/accounts/:id/identity_proofs", AccountController, :identity_proofs)
get("/follow_requests", FollowRequestController, :index)
get("/blocks", AccountController, :blocks)
@ -657,6 +658,17 @@ defmodule Pleroma.Web.Router do
end
end
# Test-only routes needed to test action dispatching and plug chain execution
if Pleroma.Config.get(:env) == :test do
scope "/test/authenticated_api", Pleroma.Tests do
pipe_through(:authenticated_api)
for action <- [:skipped_oauth, :performed_oauth, :missed_oauth] do
get("/#{action}", OAuthTestController, action)
end
end
end
scope "/", Pleroma.Web.MongooseIM do
get("/user_exists", MongooseIMController, :user_exists)
get("/check_password", MongooseIMController, :check_password)

View file

@ -15,6 +15,8 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read)
plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token])
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
action_fallback(:errors)

View file

@ -29,11 +29,40 @@ defmodule Pleroma.Web do
import Pleroma.Web.Router.Helpers
import Pleroma.Web.TranslationHelpers
alias Pleroma.Plugs.PlugHelper
plug(:set_put_layout)
defp set_put_layout(conn, _) do
put_layout(conn, Pleroma.Config.get(:app_layout, "app.html"))
end
# Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain
defp skip_plug(conn, plug_module) do
try do
plug_module.ensure_skippable()
rescue
UndefinedFunctionError ->
raise "#{plug_module} is not skippable. Append `use Pleroma.Web, :plug` to its code."
end
PlugHelper.append_to_skipped_plugs(conn, plug_module)
end
# Here we can apply before-action hooks (e.g. verify whether auth checks were preformed)
defp action(conn, params) do
if Pleroma.Plugs.AuthExpectedPlug.auth_expected?(conn) &&
not PlugHelper.plug_called_or_skipped?(conn, Pleroma.Plugs.OAuthScopesPlug) do
conn
|> render_error(
:forbidden,
"Security violation: OAuth scopes check was neither handled nor explicitly skipped."
)
|> halt()
else
super(conn, params)
end
end
end
end
@ -96,6 +125,26 @@ defmodule Pleroma.Web do
end
end
def plug do
quote do
alias Pleroma.Plugs.PlugHelper
def ensure_skippable, do: :noop
@impl Plug
@doc "If marked as skipped, returns `conn`, and calls `perform/2` otherwise."
def call(%Plug.Conn{} = conn, options) do
if PlugHelper.plug_skipped?(conn, __MODULE__) do
conn
else
conn
|> PlugHelper.append_to_called_plugs(__MODULE__)
|> perform(options)
end
end
end
end
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""