Automatic checks of authentication / instance publicity. Definition of missing OAuth scopes in AdminAPIController. Refactoring.

This commit is contained in:
Ivan Tashkinov 2020-04-21 16:29:19 +03:00
parent 3c828016d9
commit f685cbd309
44 changed files with 355 additions and 267 deletions

33
docs/dev.md Normal file
View file

@ -0,0 +1,33 @@
This document contains notes and guidelines for Pleroma developers.
# Authentication & Authorization
## OAuth token-based authentication & authorization
* Pleroma supports hierarchical OAuth scopes, just like Mastodon but with added granularity of admin scopes.
For a reference, see [Mastodon OAuth scopes](https://docs.joinmastodon.org/api/oauth-scopes/).
* It is important to either define OAuth scope restrictions or explicitly mark OAuth scope check as skipped, for every
controller action. To define scopes, call `plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: [...]})`. To explicitly set
OAuth scopes check skipped, call `plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug <when ...>)`.
* In controllers, `use Pleroma.Web, :controller` will result in `action/2` (see `Pleroma.Web.controller/0` for definition)
be called prior to actual controller action, and it'll perform security / privacy checks before passing control to
actual controller action. For routes with `:authenticated_api` pipeline, authentication & authorization are expected,
thus `OAuthScopesPlug` will be run unless explicitly skipped (also `EnsureAuthenticatedPlug` will be executed
immediately before action even if there was an early run to give an early error, since `OAuthScopesPlug` supports
`:proceed_unauthenticated` option, and other plugs may support similar options as well). For `:api` pipeline routes,
`EnsurePublicOrAuthenticatedPlug` will be called to ensure that the instance is not private or user is authenticated
(unless explicitly skipped). Such automated checks help to prevent human errors and result in higher security / privacy
for users.
## [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization)
* With HTTP Basic Auth, OAuth scopes check is _not_ performed for any action (since password is provided during the auth,
requester is able to obtain a token with full permissions anyways). `Pleroma.Plugs.AuthenticationPlug` and
`Pleroma.Plugs.LegacyAuthenticationPlug` both call `Pleroma.Plugs.OAuthScopesPlug.skip_plug(conn)` when password
is provided.
## Auth-related configuration, OAuth consumer mode etc.
See `Authentication` section of [`docs/configuration/cheatsheet.md`](docs/configuration/cheatsheet.md#authentication).

View file

@ -1,17 +0,0 @@
# 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

@ -5,17 +5,21 @@
defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do
import Plug.Conn import Plug.Conn
import Pleroma.Web.TranslationHelpers import Pleroma.Web.TranslationHelpers
alias Pleroma.User alias Pleroma.User
use Pleroma.Web, :plug
def init(options) do def init(options) do
options options
end end
def call(%{assigns: %{user: %User{}}} = conn, _) do @impl true
def perform(%{assigns: %{user: %User{}}} = conn, _) do
conn conn
end end
def call(conn, options) do def perform(conn, options) do
perform = perform =
cond do cond do
options[:if_func] -> options[:if_func].() options[:if_func] -> options[:if_func].()

View file

@ -5,14 +5,18 @@
defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do
import Pleroma.Web.TranslationHelpers import Pleroma.Web.TranslationHelpers
import Plug.Conn import Plug.Conn
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.User alias Pleroma.User
use Pleroma.Web, :plug
def init(options) do def init(options) do
options options
end end
def call(conn, _) do @impl true
def perform(conn, _) do
public? = Config.get!([:instance, :public]) public? = Config.get!([:instance, :public])
case {public?, conn} do case {public?, conn} do

View file

@ -0,0 +1,20 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.ExpectAuthenticatedCheckPlug do
@moduledoc """
Marks `Pleroma.Plugs.EnsureAuthenticatedPlug` as expected to be executed later in plug chain.
No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`).
"""
use Pleroma.Web, :plug
def init(options), do: options
@impl true
def perform(conn, _) do
conn
end
end

View file

@ -0,0 +1,21 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug do
@moduledoc """
Marks `Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug` as expected to be executed later in plug
chain.
No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`).
"""
use Pleroma.Web, :plug
def init(options), do: options
@impl true
def perform(conn, _) do
conn
end
end

View file

@ -7,15 +7,12 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
import Pleroma.Web.Gettext import Pleroma.Web.Gettext
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.PlugHelper
use Pleroma.Web, :plug use Pleroma.Web, :plug
@behaviour Plug
def init(%{scopes: _} = options), do: options def init(%{scopes: _} = options), do: options
@impl true
def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
op = options[:op] || :| op = options[:op] || :|
token = assigns[:token] token = assigns[:token]
@ -34,7 +31,6 @@ def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
conn conn
|> assign(:user, nil) |> assign(:user, nil)
|> assign(:token, nil) |> assign(:token, nil)
|> maybe_perform_instance_privacy_check(options)
true -> true ->
missing_scopes = scopes -- matched_scopes missing_scopes = scopes -- matched_scopes
@ -71,12 +67,4 @@ def transform_scopes(scopes, options) do
scopes scopes
end end
end end
defp maybe_perform_instance_privacy_check(%Plug.Conn{} = conn, options) do
if options[:skip_instance_privacy_check] do
conn
else
EnsurePublicOrAuthenticatedPlug.call(conn, [])
end
end
end end

View file

@ -48,6 +48,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
%{scopes: ["write:accounts"], admin: true} %{scopes: ["write:accounts"], admin: true}
when action in [ when action in [
:get_password_reset, :get_password_reset,
:force_password_reset,
:user_delete, :user_delete,
:users_create, :users_create,
:user_toggle_activation, :user_toggle_activation,
@ -56,7 +57,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
:tag_users, :tag_users,
:untag_users, :untag_users,
:right_add, :right_add,
:right_add_multiple,
:right_delete, :right_delete,
:right_delete_multiple,
:update_user_credentials :update_user_credentials
] ]
) )
@ -84,13 +87,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write:reports"], admin: true} %{scopes: ["write:reports"], admin: true}
when action in [:reports_update] when action in [:reports_update, :report_notes_create, :report_notes_delete]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:statuses"], admin: true} %{scopes: ["read:statuses"], admin: true}
when action == :list_user_statuses when action in [:list_statuses, :list_user_statuses, :list_instance_statuses]
) )
plug( plug(
@ -102,13 +105,30 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read"], admin: true} %{scopes: ["read"], admin: true}
when action in [:config_show, :list_log, :stats] when action in [
:config_show,
:list_log,
:stats,
:relay_list,
:config_descriptions,
:need_reboot
]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write"], admin: true} %{scopes: ["write"], admin: true}
when action == :config_update when action in [
:restart,
:config_update,
:resend_confirmation_email,
:confirm_email,
:oauth_app_create,
:oauth_app_list,
:oauth_app_update,
:oauth_app_delete,
:reload_emoji
]
) )
action_fallback(:errors) action_fallback(:errors)
@ -1103,25 +1123,25 @@ def stats(conn, _) do
|> json(%{"status_visibility" => count}) |> json(%{"status_visibility" => count})
end end
def errors(conn, {:error, :not_found}) do defp errors(conn, {:error, :not_found}) do
conn conn
|> put_status(:not_found) |> put_status(:not_found)
|> json(dgettext("errors", "Not found")) |> json(dgettext("errors", "Not found"))
end end
def errors(conn, {:error, reason}) do defp errors(conn, {:error, reason}) do
conn conn
|> put_status(:bad_request) |> put_status(:bad_request)
|> json(reason) |> json(reason)
end end
def errors(conn, {:param_cast, _}) do defp errors(conn, {:param_cast, _}) do
conn conn
|> put_status(:bad_request) |> put_status(:bad_request)
|> json(dgettext("errors", "Invalid parameters")) |> json(dgettext("errors", "Invalid parameters"))
end end
def errors(conn, _) do defp errors(conn, _) do
conn conn
|> put_status(:internal_server_error) |> put_status(:internal_server_error)
|> json(dgettext("errors", "Something went wrong")) |> json(dgettext("errors", "Something went wrong"))

View file

@ -4,7 +4,9 @@
defmodule Fallback.RedirectController do defmodule Fallback.RedirectController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
require Logger require Logger
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.Metadata alias Pleroma.Web.Metadata

View file

@ -13,11 +13,14 @@ defmodule Pleroma.Web.MastoFEController do
# Note: :index action handles attempt of unauthenticated access to private instance with redirect # Note: :index action handles attempt of unauthenticated access to private instance with redirect
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read"], fallback: :proceed_unauthenticated, skip_instance_privacy_check: true} %{scopes: ["read"], fallback: :proceed_unauthenticated}
when action == :index when action == :index
) )
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :manifest]) plug(
:skip_plug,
Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :manifest]
)
@doc "GET /web/*path" @doc "GET /web/*path"
def index(%{assigns: %{user: user, token: token}} = conn, _params) def index(%{assigns: %{user: user, token: token}} = conn, _params)

View file

@ -26,12 +26,24 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.TwitterAPI.TwitterAPI
plug(:skip_plug, OAuthScopesPlug when action == :identity_proofs) plug(:skip_plug, OAuthScopesPlug when action in [:create, :identity_proofs])
plug(
:skip_plug,
Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
when action in [:create, :show, :statuses]
)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]} %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
when action == :show when action in [:show, :endorsements]
)
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
when action == :statuses
) )
plug( plug(
@ -56,21 +68,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships) plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
# Note: :follows (POST /api/v1/follows) is the same as :follow, consider removing :follows
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["follow", "write:follows"]} when action in [:follows, :follow, :unfollow] %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
) )
plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes) plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute]) plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
plug(
Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
when action not in [:create, :show, :statuses]
)
@relationship_actions [:follow, :unfollow] @relationship_actions [:follow, :unfollow]
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
@ -356,7 +362,7 @@ def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
end end
@doc "POST /api/v1/follows" @doc "POST /api/v1/follows"
def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do def follow_by_uri(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)}, with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
{_, true} <- {:followed, follower.id != followed.id}, {_, true} <- {:followed, follower.id != followed.id},
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do

View file

@ -13,10 +13,10 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
@local_mastodon_name "Mastodon-Local"
plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset) plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset)
@local_mastodon_name "Mastodon-Local"
@doc "GET /web/login" @doc "GET /web/login"
def login(%{assigns: %{user: %User{}}} = conn, _params) do def login(%{assigns: %{user: %User{}}} = conn, _params) do
redirect(conn, to: local_mastodon_root_path(conn)) redirect(conn, to: local_mastodon_root_path(conn))

View file

@ -14,9 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :index) plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :index)
plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action == :read) plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action != :index)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
@doc "GET /api/v1/conversations" @doc "GET /api/v1/conversations"
def index(%{assigns: %{user: user}} = conn, params) do def index(%{assigns: %{user: user}} = conn, params) do
@ -28,7 +26,7 @@ def index(%{assigns: %{user: user}} = conn, params) do
end end
@doc "POST /api/v1/conversations/:id/read" @doc "POST /api/v1/conversations/:id/read"
def read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do def mark_as_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
with %Participation{} = participation <- with %Participation{} = participation <-
Repo.get_by(Participation, id: participation_id, user_id: user.id), Repo.get_by(Participation, id: participation_id, user_id: user.id),
{:ok, participation} <- Participation.mark_as_read(participation) do {:ok, participation} <- Participation.mark_as_read(participation) do

View file

@ -21,8 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
%{scopes: ["follow", "write:blocks"]} when action != :index %{scopes: ["follow", "write:blocks"]} when action != :index
) )
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
@doc "GET /api/v1/domain_blocks" @doc "GET /api/v1/domain_blocks"
def index(%{assigns: %{user: user}} = conn, _) do def index(%{assigns: %{user: user}} = conn, _) do
json(conn, Map.get(user, :domain_blocks, [])) json(conn, Map.get(user, :domain_blocks, []))

View file

@ -17,8 +17,6 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
%{scopes: ["write:filters"]} when action not in @oauth_read_actions %{scopes: ["write:filters"]} when action not in @oauth_read_actions
) )
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
@doc "GET /api/v1/filters" @doc "GET /api/v1/filters"
def index(%{assigns: %{user: user}} = conn, _) do def index(%{assigns: %{user: user}} = conn, _) do
filters = Filter.get_filters(user) filters = Filter.get_filters(user)

View file

@ -21,8 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
%{scopes: ["follow", "write:follows"]} when action != :index %{scopes: ["follow", "write:follows"]} when action != :index
) )
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
@doc "GET /api/v1/follow_requests" @doc "GET /api/v1/follow_requests"
def index(%{assigns: %{user: followed}} = conn, _params) do def index(%{assigns: %{user: followed}} = conn, _params) do
follow_requests = User.get_follow_requests(followed) follow_requests = User.get_follow_requests(followed)

View file

@ -11,16 +11,16 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
plug(:list_by_id_and_user when action not in [:index, :create]) plug(:list_by_id_and_user when action not in [:index, :create])
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in [:index, :show, :list_accounts]) @oauth_read_actions [:index, :show, :list_accounts]
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in @oauth_read_actions)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write:lists"]} %{scopes: ["write:lists"]}
when action in [:create, :update, :delete, :add_to_list, :remove_from_list] when action not in @oauth_read_actions
) )
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
# GET /api/v1/lists # GET /api/v1/lists

View file

@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do
) )
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :upsert) plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :upsert)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
# GET /api/v1/markers # GET /api/v1/markers

View file

@ -17,8 +17,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug when action in [:empty_array, :empty_object]) plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug when action in [:empty_array, :empty_object])
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
def empty_array(conn, _) do def empty_array(conn, _) do

View file

@ -15,8 +15,6 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
plug(OAuthScopesPlug, %{scopes: ["write:media"]}) plug(OAuthScopesPlug, %{scopes: ["write:media"]})
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
@doc "POST /api/v1/media" @doc "POST /api/v1/media"
def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
with {:ok, object} <- with {:ok, object} <-

View file

@ -20,8 +20,6 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions) plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
# GET /api/v1/notifications # GET /api/v1/notifications
def index(conn, %{"account_id" => account_id} = params) do def index(conn, %{"account_id" => account_id} = params) do
case Pleroma.User.get_cached_by_id(account_id) do case Pleroma.User.get_cached_by_id(account_id) do

View file

@ -22,8 +22,6 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :vote) plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :vote)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
@doc "GET /api/v1/polls/:id" @doc "GET /api/v1/polls/:id"
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60), with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),

View file

@ -11,8 +11,6 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create) plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
@doc "POST /api/v1/reports" @doc "POST /api/v1/reports"
def create(%{assigns: %{user: user}} = conn, params) do def create(%{assigns: %{user: user}} = conn, params) do
with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do

View file

@ -18,8 +18,6 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions) plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions)
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions) plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
@doc "GET /api/v1/scheduled_statuses" @doc "GET /api/v1/scheduled_statuses"

View file

@ -21,8 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
# Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search) # Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated}) plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated})
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search]) plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do

View file

@ -77,7 +77,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark] %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
) )
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :show]) plug(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show])
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a

View file

@ -12,7 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
action_fallback(:errors) action_fallback(:errors)
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]}) plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
plug(:restrict_push_enabled) plug(:restrict_push_enabled)
# Creates PushSubscription # Creates PushSubscription

View file

@ -26,7 +26,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct]) plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list) plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :public) plug(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action == :public)
plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.MediaProxy.MediaProxyController do defmodule Pleroma.Web.MediaProxy.MediaProxyController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.ReverseProxy alias Pleroma.ReverseProxy
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy

View file

@ -17,6 +17,13 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
require Pleroma.Constants require Pleroma.Constants
plug(:skip_plug, OAuthScopesPlug when action == :confirmation_resend)
plug(
:skip_plug,
Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action == :confirmation_resend
)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe] %{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]
@ -35,13 +42,8 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites) plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
# An extra safety measure for possible actions not guarded by OAuth permissions specification
plug(
Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
when action != :confirmation_resend
)
plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend) plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend)
plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe]) plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)

View file

@ -1,6 +1,7 @@
defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
require Logger require Logger
@ -11,17 +12,20 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
when action in [ when action in [
:create, :create,
:delete, :delete,
:download_from, :save_from,
:list_from,
:import_from_fs, :import_from_fs,
:update_file, :update_file,
:update_metadata :update_metadata
] ]
) )
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) plug(
:skip_plug,
[OAuthScopesPlug, ExpectPublicOrAuthenticatedCheckPlug]
when action in [:download_shared, :list_packs, :list_from]
)
def emoji_dir_path do defp emoji_dir_path do
Path.join( Path.join(
Pleroma.Config.get!([:instance, :static_dir]), Pleroma.Config.get!([:instance, :static_dir]),
"emoji" "emoji"
@ -212,13 +216,13 @@ defp shareable_packs_available(address) do
end end
@doc """ @doc """
An admin endpoint to request downloading a pack named `pack_name` from the instance An admin endpoint to request downloading and storing a pack named `pack_name` from the instance
`instance_address`. `instance_address`.
If the requested instance's admin chose to share the pack, it will be downloaded If the requested instance's admin chose to share the pack, it will be downloaded
from that instance, otherwise it will be downloaded from the fallback source, if there is one. from that instance, otherwise it will be downloaded from the fallback source, if there is one.
""" """
def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do def save_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
address = String.trim(address) address = String.trim(address)
if shareable_packs_available(address) do if shareable_packs_available(address) do

View file

@ -12,8 +12,6 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show) plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
@doc "GET /api/v1/pleroma/mascot" @doc "GET /api/v1/pleroma/mascot"
def show(%{assigns: %{user: user}} = conn, _params) do def show(%{assigns: %{user: user}} = conn, _params) do
json(conn, User.get_mascot(user)) json(conn, User.get_mascot(user))

View file

@ -34,12 +34,14 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write:conversations"]} when action in [:update_conversation, :read_conversations] %{scopes: ["write:conversations"]}
when action in [:update_conversation, :mark_conversations_as_read]
) )
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification) plug(
OAuthScopesPlug,
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read
)
def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id} = params) do def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id} = params) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
@ -167,7 +169,7 @@ def update_conversation(
end end
end end
def read_conversations(%{assigns: %{user: user}} = conn, _params) do def mark_conversations_as_read(%{assigns: %{user: user}} = conn, _params) do
with {:ok, _, participations} <- Participation.mark_all_as_read(user) do with {:ok, _, participations} <- Participation.mark_all_as_read(user) do
conn conn
|> add_link_headers(participations) |> add_link_headers(participations)
@ -176,7 +178,7 @@ def read_conversations(%{assigns: %{user: user}} = conn, _params) do
end end
end end
def read_notification(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
with {:ok, notification} <- Notification.read_one(user, notification_id) do with {:ok, notification} <- Notification.read_one(user, notification_id) do
conn conn
|> put_view(NotificationView) |> put_view(NotificationView)
@ -189,7 +191,7 @@ def read_notification(%{assigns: %{user: user}} = conn, %{"id" => notification_i
end end
end end
def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id} = params) do def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"max_id" => max_id} = params) do
with notifications <- Notification.set_read_up_to(user, max_id) do with notifications <- Notification.set_read_up_to(user, max_id) do
notifications = Enum.take(notifications, 80) notifications = Enum.take(notifications, 80)

View file

@ -16,8 +16,6 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :user_scrobbles) plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :user_scrobbles)
plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles) plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do
params = params =
if !params["length"] do if !params["length"] do

View file

@ -16,6 +16,14 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.UserEnabledPlug) plug(Pleroma.Plugs.UserEnabledPlug)
end end
pipeline :expect_authentication do
plug(Pleroma.Plugs.ExpectAuthenticatedCheckPlug)
end
pipeline :expect_public_instance_or_authentication do
plug(Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug)
end
pipeline :authenticate do pipeline :authenticate do
plug(Pleroma.Plugs.OAuthPlug) plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug) plug(Pleroma.Plugs.BasicAuthDecoderPlug)
@ -39,20 +47,22 @@ defmodule Pleroma.Web.Router do
end end
pipeline :api do pipeline :api do
plug(:expect_public_instance_or_authentication)
plug(:base_api) plug(:base_api)
plug(:after_auth) plug(:after_auth)
plug(Pleroma.Plugs.IdempotencyPlug) plug(Pleroma.Plugs.IdempotencyPlug)
end end
pipeline :authenticated_api do pipeline :authenticated_api do
plug(:expect_authentication)
plug(:base_api) plug(:base_api)
plug(Pleroma.Plugs.AuthExpectedPlug)
plug(:after_auth) plug(:after_auth)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug) plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.IdempotencyPlug) plug(Pleroma.Plugs.IdempotencyPlug)
end end
pipeline :admin_api do pipeline :admin_api do
plug(:expect_authentication)
plug(:base_api) plug(:base_api)
plug(Pleroma.Plugs.AdminSecretAuthenticationPlug) plug(Pleroma.Plugs.AdminSecretAuthenticationPlug)
plug(:after_auth) plug(:after_auth)
@ -200,24 +210,28 @@ defmodule Pleroma.Web.Router do
end end
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
scope "/packs" do
# Modifying packs # Modifying packs
scope "/packs" do
pipe_through(:admin_api) pipe_through(:admin_api)
post("/import_from_fs", EmojiAPIController, :import_from_fs) post("/import_from_fs", EmojiAPIController, :import_from_fs)
post("/:pack_name/update_file", EmojiAPIController, :update_file) post("/:pack_name/update_file", EmojiAPIController, :update_file)
post("/:pack_name/update_metadata", EmojiAPIController, :update_metadata) post("/:pack_name/update_metadata", EmojiAPIController, :update_metadata)
put("/:name", EmojiAPIController, :create) put("/:name", EmojiAPIController, :create)
delete("/:name", EmojiAPIController, :delete) delete("/:name", EmojiAPIController, :delete)
post("/download_from", EmojiAPIController, :download_from)
post("/list_from", EmojiAPIController, :list_from) # Note: /download_from downloads and saves to instance, not to requester
post("/download_from", EmojiAPIController, :save_from)
end end
scope "/packs" do
# Pack info / downloading # Pack info / downloading
scope "/packs" do
get("/", EmojiAPIController, :list_packs) get("/", EmojiAPIController, :list_packs)
get("/:name/download_shared/", EmojiAPIController, :download_shared) get("/:name/download_shared/", EmojiAPIController, :download_shared)
get("/list_from", EmojiAPIController, :list_from)
# Deprecated: POST /api/pleroma/emoji/packs/list_from (use GET instead)
post("/list_from", EmojiAPIController, :list_from)
end end
end end
@ -277,7 +291,7 @@ defmodule Pleroma.Web.Router do
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses) get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
get("/conversations/:id", PleromaAPIController, :conversation) get("/conversations/:id", PleromaAPIController, :conversation)
post("/conversations/read", PleromaAPIController, :read_conversations) post("/conversations/read", PleromaAPIController, :mark_conversations_as_read)
end end
scope [] do scope [] do
@ -286,7 +300,7 @@ defmodule Pleroma.Web.Router do
patch("/conversations/:id", PleromaAPIController, :update_conversation) patch("/conversations/:id", PleromaAPIController, :update_conversation)
put("/statuses/:id/reactions/:emoji", PleromaAPIController, :react_with_emoji) put("/statuses/:id/reactions/:emoji", PleromaAPIController, :react_with_emoji)
delete("/statuses/:id/reactions/:emoji", PleromaAPIController, :unreact_with_emoji) delete("/statuses/:id/reactions/:emoji", PleromaAPIController, :unreact_with_emoji)
post("/notifications/read", PleromaAPIController, :read_notification) post("/notifications/read", PleromaAPIController, :mark_notifications_as_read)
patch("/accounts/update_avatar", AccountController, :update_avatar) patch("/accounts/update_avatar", AccountController, :update_avatar)
patch("/accounts/update_banner", AccountController, :update_banner) patch("/accounts/update_banner", AccountController, :update_banner)
@ -322,53 +336,81 @@ defmodule Pleroma.Web.Router do
pipe_through(:authenticated_api) pipe_through(:authenticated_api)
get("/accounts/verify_credentials", AccountController, :verify_credentials) get("/accounts/verify_credentials", AccountController, :verify_credentials)
patch("/accounts/update_credentials", AccountController, :update_credentials)
get("/accounts/relationships", AccountController, :relationships) get("/accounts/relationships", AccountController, :relationships)
get("/accounts/:id/lists", AccountController, :lists) get("/accounts/:id/lists", AccountController, :lists)
get("/accounts/:id/identity_proofs", AccountController, :identity_proofs) get("/accounts/:id/identity_proofs", AccountController, :identity_proofs)
get("/endorsements", AccountController, :endorsements)
get("/follow_requests", FollowRequestController, :index)
get("/blocks", AccountController, :blocks) get("/blocks", AccountController, :blocks)
get("/mutes", AccountController, :mutes) get("/mutes", AccountController, :mutes)
get("/timelines/home", TimelineController, :home) post("/follows", AccountController, :follow_by_uri)
get("/timelines/direct", TimelineController, :direct) post("/accounts/:id/follow", AccountController, :follow)
post("/accounts/:id/unfollow", AccountController, :unfollow)
post("/accounts/:id/block", AccountController, :block)
post("/accounts/:id/unblock", AccountController, :unblock)
post("/accounts/:id/mute", AccountController, :mute)
post("/accounts/:id/unmute", AccountController, :unmute)
get("/favourites", StatusController, :favourites) get("/conversations", ConversationController, :index)
get("/bookmarks", StatusController, :bookmarks) post("/conversations/:id/read", ConversationController, :mark_as_read)
get("/domain_blocks", DomainBlockController, :index)
post("/domain_blocks", DomainBlockController, :create)
delete("/domain_blocks", DomainBlockController, :delete)
get("/filters", FilterController, :index)
post("/filters", FilterController, :create)
get("/filters/:id", FilterController, :show)
put("/filters/:id", FilterController, :update)
delete("/filters/:id", FilterController, :delete)
get("/follow_requests", FollowRequestController, :index)
post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
post("/follow_requests/:id/reject", FollowRequestController, :reject)
get("/lists", ListController, :index)
get("/lists/:id", ListController, :show)
get("/lists/:id/accounts", ListController, :list_accounts)
delete("/lists/:id", ListController, :delete)
post("/lists", ListController, :create)
put("/lists/:id", ListController, :update)
post("/lists/:id/accounts", ListController, :add_to_list)
delete("/lists/:id/accounts", ListController, :remove_from_list)
get("/markers", MarkerController, :index)
post("/markers", MarkerController, :upsert)
post("/media", MediaController, :create)
put("/media/:id", MediaController, :update)
get("/notifications", NotificationController, :index) get("/notifications", NotificationController, :index)
get("/notifications/:id", NotificationController, :show) get("/notifications/:id", NotificationController, :show)
post("/notifications/:id/dismiss", NotificationController, :dismiss) post("/notifications/:id/dismiss", NotificationController, :dismiss)
post("/notifications/clear", NotificationController, :clear) post("/notifications/clear", NotificationController, :clear)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple) delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
# Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead # Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead
post("/notifications/dismiss", NotificationController, :dismiss) post("/notifications/dismiss", NotificationController, :dismiss)
post("/polls/:id/votes", PollController, :vote)
post("/reports", ReportController, :create)
get("/scheduled_statuses", ScheduledActivityController, :index) get("/scheduled_statuses", ScheduledActivityController, :index)
get("/scheduled_statuses/:id", ScheduledActivityController, :show) get("/scheduled_statuses/:id", ScheduledActivityController, :show)
get("/lists", ListController, :index) put("/scheduled_statuses/:id", ScheduledActivityController, :update)
get("/lists/:id", ListController, :show) delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
get("/lists/:id/accounts", ListController, :list_accounts)
get("/domain_blocks", DomainBlockController, :index) get("/favourites", StatusController, :favourites)
get("/bookmarks", StatusController, :bookmarks)
get("/filters", FilterController, :index)
get("/suggestions", SuggestionController, :index)
get("/conversations", ConversationController, :index)
post("/conversations/:id/read", ConversationController, :read)
get("/endorsements", AccountController, :endorsements)
patch("/accounts/update_credentials", AccountController, :update_credentials)
post("/statuses", StatusController, :create) post("/statuses", StatusController, :create)
delete("/statuses/:id", StatusController, :delete) delete("/statuses/:id", StatusController, :delete)
post("/statuses/:id/reblog", StatusController, :reblog) post("/statuses/:id/reblog", StatusController, :reblog)
post("/statuses/:id/unreblog", StatusController, :unreblog) post("/statuses/:id/unreblog", StatusController, :unreblog)
post("/statuses/:id/favourite", StatusController, :favourite) post("/statuses/:id/favourite", StatusController, :favourite)
@ -380,49 +422,15 @@ defmodule Pleroma.Web.Router do
post("/statuses/:id/mute", StatusController, :mute_conversation) post("/statuses/:id/mute", StatusController, :mute_conversation)
post("/statuses/:id/unmute", StatusController, :unmute_conversation) post("/statuses/:id/unmute", StatusController, :unmute_conversation)
put("/scheduled_statuses/:id", ScheduledActivityController, :update)
delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
post("/polls/:id/votes", PollController, :vote)
post("/media", MediaController, :create)
put("/media/:id", MediaController, :update)
delete("/lists/:id", ListController, :delete)
post("/lists", ListController, :create)
put("/lists/:id", ListController, :update)
post("/lists/:id/accounts", ListController, :add_to_list)
delete("/lists/:id/accounts", ListController, :remove_from_list)
post("/filters", FilterController, :create)
get("/filters/:id", FilterController, :show)
put("/filters/:id", FilterController, :update)
delete("/filters/:id", FilterController, :delete)
post("/reports", ReportController, :create)
post("/follows", AccountController, :follows)
post("/accounts/:id/follow", AccountController, :follow)
post("/accounts/:id/unfollow", AccountController, :unfollow)
post("/accounts/:id/block", AccountController, :block)
post("/accounts/:id/unblock", AccountController, :unblock)
post("/accounts/:id/mute", AccountController, :mute)
post("/accounts/:id/unmute", AccountController, :unmute)
post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
post("/follow_requests/:id/reject", FollowRequestController, :reject)
post("/domain_blocks", DomainBlockController, :create)
delete("/domain_blocks", DomainBlockController, :delete)
post("/push/subscription", SubscriptionController, :create) post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :get) get("/push/subscription", SubscriptionController, :get)
put("/push/subscription", SubscriptionController, :update) put("/push/subscription", SubscriptionController, :update)
delete("/push/subscription", SubscriptionController, :delete) delete("/push/subscription", SubscriptionController, :delete)
get("/markers", MarkerController, :index) get("/suggestions", SuggestionController, :index)
post("/markers", MarkerController, :upsert)
get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct)
end end
scope "/api/web", Pleroma.Web do scope "/api/web", Pleroma.Web do
@ -507,7 +515,11 @@ defmodule Pleroma.Web.Router do
get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens) get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)
delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token) delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token)
post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read) post(
"/qvitter/statuses/notifications/read",
TwitterAPI.Controller,
:mark_notifications_as_read
)
end end
pipeline :ostatus do pipeline :ostatus do

View file

@ -25,13 +25,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
when action == :follow_import when action == :follow_import
) )
# Note: follower can submit the form (with password auth) not being signed in (having no token)
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]}
when action == :do_remote_follow
)
plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks_import) plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks_import)
plug( plug(

View file

@ -13,12 +13,13 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
require Logger require Logger
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read) plug(
OAuthScopesPlug,
%{scopes: ["write:notifications"]} when action == :mark_notifications_as_read
)
plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token]) plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token])
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
action_fallback(:errors) action_fallback(:errors)
def confirm_email(conn, %{"user_id" => uid, "token" => token}) do def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
@ -64,7 +65,10 @@ defp json_reply(conn, status, json) do
|> send_resp(status, json) |> send_resp(status, json)
end end
def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do def mark_notifications_as_read(
%{assigns: %{user: user}} = conn,
%{"latest_id" => latest_id} = params
) do
Notification.set_read_up_to(user, latest_id) Notification.set_read_up_to(user, latest_id)
notifications = Notification.for_user(user, params) notifications = Notification.for_user(user, params)
@ -75,7 +79,7 @@ def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest
|> render("index.json", %{notifications: notifications, for: user}) |> render("index.json", %{notifications: notifications, for: user})
end end
def notifications_read(%{assigns: %{user: _user}} = conn, _) do def mark_notifications_as_read(%{assigns: %{user: _user}} = conn, _) do
bad_request_reply(conn, "You need to specify latest_id") bad_request_reply(conn, "You need to specify latest_id")
end end

View file

@ -2,6 +2,11 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plug do
# Substitute for `call/2` which is defined with `use Pleroma.Web, :plug`
@callback perform(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
end
defmodule Pleroma.Web do defmodule Pleroma.Web do
@moduledoc """ @moduledoc """
A module that keeps using definitions for controllers, A module that keeps using definitions for controllers,
@ -20,44 +25,79 @@ defmodule Pleroma.Web do
below. below.
""" """
alias Pleroma.Plugs.EnsureAuthenticatedPlug
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug
alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.PlugHelper
def controller do def controller do
quote do quote do
use Phoenix.Controller, namespace: Pleroma.Web use Phoenix.Controller, namespace: Pleroma.Web
import Plug.Conn import Plug.Conn
import Pleroma.Web.Gettext import Pleroma.Web.Gettext
import Pleroma.Web.Router.Helpers import Pleroma.Web.Router.Helpers
import Pleroma.Web.TranslationHelpers import Pleroma.Web.TranslationHelpers
alias Pleroma.Plugs.PlugHelper
plug(:set_put_layout) plug(:set_put_layout)
defp set_put_layout(conn, _) do defp set_put_layout(conn, _) do
put_layout(conn, Pleroma.Config.get(:app_layout, "app.html")) put_layout(conn, Pleroma.Config.get(:app_layout, "app.html"))
end end
# Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain # Marks plugs intentionally skipped and blocks their execution if present in plugs chain
defp skip_plug(conn, plug_module) do defp skip_plug(conn, plug_modules) do
plug_modules
|> List.wrap()
|> Enum.reduce(
conn,
fn plug_module, conn ->
try do try do
plug_module.skip_plug(conn) plug_module.skip_plug(conn)
rescue rescue
UndefinedFunctionError -> UndefinedFunctionError ->
raise "#{plug_module} is not skippable. Append `use Pleroma.Web, :plug` to its code." raise "`#{plug_module}` is not skippable. Append `use Pleroma.Web, :plug` to its code."
end end
end end
)
end
# Executed just before actual controller action, invokes before-action hooks (callbacks) # Executed just before actual controller action, invokes before-action hooks (callbacks)
defp action(conn, params) do defp action(conn, params) do
with %Plug.Conn{halted: false} <- maybe_halt_on_missing_oauth_scopes_check(conn) do with %Plug.Conn{halted: false} <- maybe_perform_public_or_authenticated_check(conn),
%Plug.Conn{halted: false} <- maybe_perform_authenticated_check(conn),
%Plug.Conn{halted: false} <- maybe_halt_on_missing_oauth_scopes_check(conn) do
super(conn, params) super(conn, params)
end end
end end
# Ensures instance is public -or- user is authenticated if such check was scheduled
defp maybe_perform_public_or_authenticated_check(conn) do
if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) do
EnsurePublicOrAuthenticatedPlug.call(conn, %{})
else
conn
end
end
# Ensures user is authenticated if such check was scheduled
# Note: runs prior to action even if it was already executed earlier in plug chain
# (since OAuthScopesPlug has option of proceeding unauthenticated)
defp maybe_perform_authenticated_check(conn) do
if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) do
EnsureAuthenticatedPlug.call(conn, %{})
else
conn
end
end
# Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check # Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check
defp maybe_halt_on_missing_oauth_scopes_check(conn) do defp maybe_halt_on_missing_oauth_scopes_check(conn) do
if Pleroma.Plugs.AuthExpectedPlug.auth_expected?(conn) && if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and
not PlugHelper.plug_called_or_skipped?(conn, Pleroma.Plugs.OAuthScopesPlug) do not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
conn conn
|> render_error( |> render_error(
:forbidden, :forbidden,
@ -132,7 +172,8 @@ def channel do
def plug do def plug do
quote do quote do
alias Pleroma.Plugs.PlugHelper @behaviour Pleroma.Web.Plug
@behaviour Plug
@doc """ @doc """
Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain. Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain.
@ -146,14 +187,22 @@ def skip_plug(conn) do
end end
@impl Plug @impl Plug
@doc "If marked as skipped, returns `conn`, and calls `perform/2` otherwise." @doc """
If marked as skipped, returns `conn`, otherwise calls `perform/2`.
Note: multiple invocations of the same plug (with different or same options) are allowed.
"""
def call(%Plug.Conn{} = conn, options) do def call(%Plug.Conn{} = conn, options) do
if PlugHelper.plug_skipped?(conn, __MODULE__) do if PlugHelper.plug_skipped?(conn, __MODULE__) do
conn conn
else else
conn conn =
|> PlugHelper.append_to_private_list(PlugHelper.called_plugs_list_id(), __MODULE__) PlugHelper.append_to_private_list(
|> perform(options) conn,
PlugHelper.called_plugs_list_id(),
__MODULE__
)
apply(__MODULE__, :perform, [conn, options])
end end
end end
end end

View file

@ -20,7 +20,7 @@ test "it continues if a user is assigned", %{conn: conn} do
conn = assign(conn, :user, %User{}) conn = assign(conn, :user, %User{})
ret_conn = EnsureAuthenticatedPlug.call(conn, %{}) ret_conn = EnsureAuthenticatedPlug.call(conn, %{})
assert ret_conn == conn refute ret_conn.halted
end end
end end
@ -34,20 +34,22 @@ test "it continues if a user is assigned", %{conn: conn} do
test "it continues if a user is assigned", %{conn: conn, true_fn: true_fn, false_fn: false_fn} do test "it continues if a user is assigned", %{conn: conn, true_fn: true_fn, false_fn: false_fn} do
conn = assign(conn, :user, %User{}) conn = assign(conn, :user, %User{})
assert EnsureAuthenticatedPlug.call(conn, if_func: true_fn) == conn refute EnsureAuthenticatedPlug.call(conn, if_func: true_fn).halted
assert EnsureAuthenticatedPlug.call(conn, if_func: false_fn) == conn refute EnsureAuthenticatedPlug.call(conn, if_func: false_fn).halted
assert EnsureAuthenticatedPlug.call(conn, unless_func: true_fn) == conn refute EnsureAuthenticatedPlug.call(conn, unless_func: true_fn).halted
assert EnsureAuthenticatedPlug.call(conn, unless_func: false_fn) == conn refute EnsureAuthenticatedPlug.call(conn, unless_func: false_fn).halted
end end
test "it continues if a user is NOT assigned but :if_func evaluates to `false`", test "it continues if a user is NOT assigned but :if_func evaluates to `false`",
%{conn: conn, false_fn: false_fn} do %{conn: conn, false_fn: false_fn} do
assert EnsureAuthenticatedPlug.call(conn, if_func: false_fn) == conn ret_conn = EnsureAuthenticatedPlug.call(conn, if_func: false_fn)
refute ret_conn.halted
end end
test "it continues if a user is NOT assigned but :unless_func evaluates to `true`", test "it continues if a user is NOT assigned but :unless_func evaluates to `true`",
%{conn: conn, true_fn: true_fn} do %{conn: conn, true_fn: true_fn} do
assert EnsureAuthenticatedPlug.call(conn, unless_func: true_fn) == conn ret_conn = EnsureAuthenticatedPlug.call(conn, unless_func: true_fn)
refute ret_conn.halted
end end
test "it halts if a user is NOT assigned and :if_func evaluates to `true`", test "it halts if a user is NOT assigned and :if_func evaluates to `true`",

View file

@ -29,7 +29,7 @@ test "it continues if public", %{conn: conn} do
conn conn
|> EnsurePublicOrAuthenticatedPlug.call(%{}) |> EnsurePublicOrAuthenticatedPlug.call(%{})
assert ret_conn == conn refute ret_conn.halted
end end
test "it continues if a user is assigned, even if not public", %{conn: conn} do test "it continues if a user is assigned, even if not public", %{conn: conn} do
@ -43,6 +43,6 @@ test "it continues if a user is assigned, even if not public", %{conn: conn} do
conn conn
|> EnsurePublicOrAuthenticatedPlug.call(%{}) |> EnsurePublicOrAuthenticatedPlug.call(%{})
assert ret_conn == conn refute ret_conn.halted
end end
end end

View file

@ -5,17 +5,12 @@
defmodule Pleroma.Plugs.OAuthScopesPlugTest do defmodule Pleroma.Plugs.OAuthScopesPlugTest do
use Pleroma.Web.ConnCase, async: true use Pleroma.Web.ConnCase, async: true
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Repo alias Pleroma.Repo
import Mock import Mock
import Pleroma.Factory import Pleroma.Factory
setup_with_mocks([{EnsurePublicOrAuthenticatedPlug, [], [call: fn conn, _ -> conn end]}]) do
:ok
end
test "is not performed if marked as skipped", %{conn: conn} do test "is not performed if marked as skipped", %{conn: conn} do
with_mock OAuthScopesPlug, [:passthrough], perform: &passthrough([&1, &2]) do with_mock OAuthScopesPlug, [:passthrough], perform: &passthrough([&1, &2]) do
conn = conn =
@ -60,7 +55,7 @@ test "if `token.scopes` fulfills specified 'all of' conditions, " <>
describe "with `fallback: :proceed_unauthenticated` option, " do describe "with `fallback: :proceed_unauthenticated` option, " do
test "if `token.scopes` doesn't fulfill specified conditions, " <> test "if `token.scopes` doesn't fulfill specified conditions, " <>
"clears :user and :token assigns and calls EnsurePublicOrAuthenticatedPlug", "clears :user and :token assigns",
%{conn: conn} do %{conn: conn} do
user = insert(:user) user = insert(:user)
token1 = insert(:oauth_token, scopes: ["read", "write"], user: user) token1 = insert(:oauth_token, scopes: ["read", "write"], user: user)
@ -79,35 +74,6 @@ test "if `token.scopes` doesn't fulfill specified conditions, " <>
refute ret_conn.halted refute ret_conn.halted
refute ret_conn.assigns[:user] refute ret_conn.assigns[:user]
refute ret_conn.assigns[:token] refute ret_conn.assigns[:token]
assert called(EnsurePublicOrAuthenticatedPlug.call(ret_conn, :_))
end
end
test "with :skip_instance_privacy_check option, " <>
"if `token.scopes` doesn't fulfill specified conditions, " <>
"clears :user and :token assigns and does NOT call EnsurePublicOrAuthenticatedPlug",
%{conn: conn} do
user = insert(:user)
token1 = insert(:oauth_token, scopes: ["read:statuses", "write"], user: user)
for token <- [token1, nil], op <- [:|, :&] do
ret_conn =
conn
|> assign(:user, user)
|> assign(:token, token)
|> OAuthScopesPlug.call(%{
scopes: ["read"],
op: op,
fallback: :proceed_unauthenticated,
skip_instance_privacy_check: true
})
refute ret_conn.halted
refute ret_conn.assigns[:user]
refute ret_conn.assigns[:token]
refute called(EnsurePublicOrAuthenticatedPlug.call(ret_conn, :_))
end end
end end
end end

View file

@ -766,7 +766,7 @@ test "it requires authentication if instance is NOT federating", %{
end end
describe "POST /users/:nickname/outbox" do describe "POST /users/:nickname/outbox" do
test "it rejects posts from other users / unauuthenticated users", %{conn: conn} do test "it rejects posts from other users / unauthenticated users", %{conn: conn} do
data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!() data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)

View file

@ -38,8 +38,7 @@ test "shared & non-shared pack information in list_packs is ok" do
end end
test "listing remote packs" do test "listing remote packs" do
admin = insert(:user, is_admin: true) conn = build_conn()
%{conn: conn} = oauth_access(["admin:write"], user: admin)
resp = resp =
build_conn() build_conn()
@ -76,7 +75,7 @@ test "downloading a shared pack from download_shared" do
assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end) assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end)
end end
test "downloading shared & unshared packs from another instance via download_from, deleting them" do test "downloading shared & unshared packs from another instance, deleting them" do
on_exit(fn -> on_exit(fn ->
File.rm_rf!("#{@emoji_dir_path}/test_pack2") File.rm_rf!("#{@emoji_dir_path}/test_pack2")
File.rm_rf!("#{@emoji_dir_path}/test_pack_nonshared2") File.rm_rf!("#{@emoji_dir_path}/test_pack_nonshared2")
@ -136,7 +135,7 @@ test "downloading shared & unshared packs from another instance via download_fro
|> post( |> post(
emoji_api_path( emoji_api_path(
conn, conn,
:download_from :save_from
), ),
%{ %{
instance_address: "https://old-instance", instance_address: "https://old-instance",
@ -152,7 +151,7 @@ test "downloading shared & unshared packs from another instance via download_fro
|> post( |> post(
emoji_api_path( emoji_api_path(
conn, conn,
:download_from :save_from
), ),
%{ %{
instance_address: "https://example.com", instance_address: "https://example.com",
@ -179,7 +178,7 @@ test "downloading shared & unshared packs from another instance via download_fro
|> post( |> post(
emoji_api_path( emoji_api_path(
conn, conn,
:download_from :save_from
), ),
%{ %{
instance_address: "https://example.com", instance_address: "https://example.com",

View file

@ -19,13 +19,9 @@ test "without valid credentials", %{conn: conn} do
end end
test "with credentials, without any params" do test "with credentials, without any params" do
%{user: current_user, conn: conn} = %{conn: conn} = oauth_access(["write:notifications"])
oauth_access(["read:notifications", "write:notifications"])
conn = conn = post(conn, "/api/qvitter/statuses/notifications/read")
conn
|> assign(:user, current_user)
|> post("/api/qvitter/statuses/notifications/read")
assert json_response(conn, 400) == %{ assert json_response(conn, 400) == %{
"error" => "You need to specify latest_id", "error" => "You need to specify latest_id",