diff options
Diffstat (limited to 'lib')
43 files changed, 514 insertions, 271 deletions
diff --git a/lib/pleroma/plugs/auth_expected_plug.ex b/lib/pleroma/plugs/auth_expected_plug.ex deleted file mode 100644 index f79597dc3..000000000 --- a/lib/pleroma/plugs/auth_expected_plug.ex +++ /dev/null @@ -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 diff --git a/lib/pleroma/plugs/ensure_authenticated_plug.ex b/lib/pleroma/plugs/ensure_authenticated_plug.ex index 054d2297f..9c8f5597f 100644 --- a/lib/pleroma/plugs/ensure_authenticated_plug.ex +++ b/lib/pleroma/plugs/ensure_authenticated_plug.ex @@ -5,17 +5,21 @@  defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do    import Plug.Conn    import Pleroma.Web.TranslationHelpers +    alias Pleroma.User +  use Pleroma.Web, :plug +    def init(options) do      options    end -  def call(%{assigns: %{user: %User{}}} = conn, _) do +  @impl true +  def perform(%{assigns: %{user: %User{}}} = conn, _) do      conn    end -  def call(conn, options) do +  def perform(conn, options) do      perform =        cond do          options[:if_func] -> options[:if_func].() diff --git a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex index d980ff13d..7265bb87a 100644 --- a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex +++ b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex @@ -5,14 +5,18 @@  defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do    import Pleroma.Web.TranslationHelpers    import Plug.Conn +    alias Pleroma.Config    alias Pleroma.User +  use Pleroma.Web, :plug +    def init(options) do      options    end -  def call(conn, _) do +  @impl true +  def perform(conn, _) do      public? = Config.get!([:instance, :public])      case {public?, conn} do diff --git a/lib/pleroma/plugs/expect_authenticated_check_plug.ex b/lib/pleroma/plugs/expect_authenticated_check_plug.ex new file mode 100644 index 000000000..66b8d5de5 --- /dev/null +++ b/lib/pleroma/plugs/expect_authenticated_check_plug.ex @@ -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 diff --git a/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex b/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex new file mode 100644 index 000000000..ba0ef76bd --- /dev/null +++ b/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex @@ -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 diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex index 66f48c28c..efc25b79f 100644 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ b/lib/pleroma/plugs/oauth_scopes_plug.ex @@ -7,15 +7,12 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do    import Pleroma.Web.Gettext    alias Pleroma.Config -  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug -  alias Pleroma.Plugs.PlugHelper    use Pleroma.Web, :plug -  @behaviour Plug -    def init(%{scopes: _} = options), do: options +  @impl true    def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do      op = options[:op] || :|      token = assigns[:token] @@ -31,10 +28,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do          conn        options[:fallback] == :proceed_unauthenticated -> -        conn -        |> assign(:user, nil) -        |> assign(:token, nil) -        |> maybe_perform_instance_privacy_check(options) +        drop_auth_info(conn)        true ->          missing_scopes = scopes -- matched_scopes @@ -50,6 +44,15 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do      end    end +  @doc "Drops authentication info from connection" +  def drop_auth_info(conn) do +    # To simplify debugging, setting a private variable on `conn` if auth info is dropped +    conn +    |> put_private(:authentication_ignored, true) +    |> assign(:user, nil) +    |> assign(:token, nil) +  end +    @doc "Filters descendants of supported scopes"    def filter_descendants(scopes, supported_scopes) do      Enum.filter( @@ -71,12 +74,4 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do        scopes      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 diff --git a/lib/pleroma/tests/auth_test_controller.ex b/lib/pleroma/tests/auth_test_controller.ex new file mode 100644 index 000000000..fb04411d9 --- /dev/null +++ b/lib/pleroma/tests/auth_test_controller.ex @@ -0,0 +1,93 @@ +# 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. +defmodule Pleroma.Tests.AuthTestController do +  @moduledoc false + +  use Pleroma.Web, :controller + +  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug +  alias Pleroma.Plugs.OAuthScopesPlug +  alias Pleroma.User + +  # Serves only with proper OAuth token (:api and :authenticated_api) +  # Skipping EnsurePublicOrAuthenticatedPlug has no effect in this case +  # +  # Suggested use case: all :authenticated_api endpoints (makes no sense for :api endpoints) +  plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :do_oauth_check) + +  # Via :api, keeps :user if token has requested scopes (if :user is dropped, serves if public) +  # Via :authenticated_api, serves if token is present and has requested scopes +  # +  # Suggested use case: vast majority of :api endpoints (no sense for :authenticated_api ones) +  plug( +    OAuthScopesPlug, +    %{scopes: ["read"], fallback: :proceed_unauthenticated} +    when action == :fallback_oauth_check +  ) + +  # Keeps :user if present, executes regardless of token / token scopes +  # Fails with no :user for :authenticated_api / no user for :api on private instance +  # Note: EnsurePublicOrAuthenticatedPlug is not skipped (private instance fails on no :user) +  # Note: Basic Auth processing results in :skip_plug call for OAuthScopesPlug +  # +  # Suggested use: suppressing OAuth checks for other auth mechanisms (like Basic Auth) +  # For controller-level use, see :skip_oauth_skip_publicity_check instead +  plug( +    :skip_plug, +    OAuthScopesPlug when action == :skip_oauth_check +  ) + +  # (Shouldn't be executed since the plug is skipped) +  plug(OAuthScopesPlug, %{scopes: ["admin"]} when action == :skip_oauth_check) + +  # Via :api, keeps :user if token has requested scopes, and continues with nil :user otherwise +  # Via :authenticated_api, serves if token is present and has requested scopes +  # +  # Suggested use: as :fallback_oauth_check but open with nil :user for :api on private instances +  plug( +    :skip_plug, +    EnsurePublicOrAuthenticatedPlug when action == :fallback_oauth_skip_publicity_check +  ) + +  plug( +    OAuthScopesPlug, +    %{scopes: ["read"], fallback: :proceed_unauthenticated} +    when action == :fallback_oauth_skip_publicity_check +  ) + +  # Via :api, keeps :user if present, serves regardless of token presence / scopes / :user presence +  # Via :authenticated_api, serves if :user is set (regardless of token presence and its scopes) +  # +  # Suggested use: making an :api endpoint always accessible (e.g. email confirmation endpoint) +  plug( +    :skip_plug, +    [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] +    when action == :skip_oauth_skip_publicity_check +  ) + +  # Via :authenticated_api, always fails with 403 (endpoint is insecure) +  # Via :api, drops :user if present and serves if public (private instance rejects on no user) +  # +  # Suggested use: none; please define OAuth rules for all :api / :authenticated_api endpoints +  plug(:skip_plug, [] when action == :missing_oauth_check_definition) + +  def do_oauth_check(conn, _params), do: conn_state(conn) + +  def fallback_oauth_check(conn, _params), do: conn_state(conn) + +  def skip_oauth_check(conn, _params), do: conn_state(conn) + +  def fallback_oauth_skip_publicity_check(conn, _params), do: conn_state(conn) + +  def skip_oauth_skip_publicity_check(conn, _params), do: conn_state(conn) + +  def missing_oauth_check_definition(conn, _params), do: conn_state(conn) + +  defp conn_state(%{assigns: %{user: %User{} = user}} = conn), +    do: json(conn, %{user_id: user.id}) + +  defp conn_state(conn), do: json(conn, %{user_id: nil}) +end diff --git a/lib/pleroma/tests/oauth_test_controller.ex b/lib/pleroma/tests/oauth_test_controller.ex deleted file mode 100644 index 58d517f78..000000000 --- a/lib/pleroma/tests/oauth_test_controller.ex +++ /dev/null @@ -1,31 +0,0 @@ -# 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 diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 9c79310c0..816c11e01 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -48,6 +48,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      %{scopes: ["write:accounts"], admin: true}      when action in [             :get_password_reset, +           :force_password_reset,             :user_delete,             :users_create,             :user_toggle_activation, @@ -56,7 +57,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do             :tag_users,             :untag_users,             :right_add, +           :right_add_multiple,             :right_delete, +           :right_delete_multiple,             :update_user_credentials           ]    ) @@ -84,13 +87,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    plug(      OAuthScopesPlug,      %{scopes: ["write:reports"], admin: true} -    when action in [:reports_update] +    when action in [:reports_update, :report_notes_create, :report_notes_delete]    )    plug(      OAuthScopesPlug,      %{scopes: ["read:statuses"], admin: true} -    when action == :list_user_statuses +    when action in [:list_statuses, :list_user_statuses, :list_instance_statuses]    )    plug( @@ -102,13 +105,30 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    plug(      OAuthScopesPlug,      %{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(      OAuthScopesPlug,      %{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) @@ -1103,25 +1123,25 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      |> json(%{"status_visibility" => count})    end -  def errors(conn, {:error, :not_found}) do +  defp errors(conn, {:error, :not_found}) do      conn      |> put_status(:not_found)      |> json(dgettext("errors", "Not found"))    end -  def errors(conn, {:error, reason}) do +  defp errors(conn, {:error, reason}) do      conn      |> put_status(:bad_request)      |> json(reason)    end -  def errors(conn, {:param_cast, _}) do +  defp errors(conn, {:param_cast, _}) do      conn      |> put_status(:bad_request)      |> json(dgettext("errors", "Invalid parameters"))    end -  def errors(conn, _) do +  defp errors(conn, _) do      conn      |> put_status(:internal_server_error)      |> json(dgettext("errors", "Something went wrong")) diff --git a/lib/pleroma/web/fallback_redirect_controller.ex b/lib/pleroma/web/fallback_redirect_controller.ex index c13518030..0d9d578fc 100644 --- a/lib/pleroma/web/fallback_redirect_controller.ex +++ b/lib/pleroma/web/fallback_redirect_controller.ex @@ -4,7 +4,9 @@  defmodule Fallback.RedirectController do    use Pleroma.Web, :controller +    require Logger +    alias Pleroma.User    alias Pleroma.Web.Metadata diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex index 557cde328..d0d8bc8eb 100644 --- a/lib/pleroma/web/masto_fe_controller.ex +++ b/lib/pleroma/web/masto_fe_controller.ex @@ -5,19 +5,25 @@  defmodule Pleroma.Web.MastoFEController do    use Pleroma.Web, :controller +  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.User    plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :put_settings)    # Note: :index action handles attempt of unauthenticated access to private instance with redirect +  plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action == :index) +    plug(      OAuthScopesPlug, -    %{scopes: ["read"], fallback: :proceed_unauthenticated, skip_instance_privacy_check: true} +    %{scopes: ["read"], fallback: :proceed_unauthenticated}      when action == :index    ) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :manifest]) +  plug( +    :skip_plug, +    [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :manifest +  )    @doc "GET /web/*path"    def index(%{assigns: %{user: user, token: token}} = conn, _params) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 37adeec5f..1eedf02d6 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do        skip_relationships?: 1      ] +  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.Plugs.RateLimiter    alias Pleroma.User @@ -28,18 +29,26 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError) -  plug(:skip_plug, OAuthScopesPlug when action == :identity_proofs) +  plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create) + +  plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])    plug(      OAuthScopesPlug,      %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]} -    when action == :show +    when action in [:show, :followers, :following] +  ) + +  plug( +    OAuthScopesPlug, +    %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]} +    when action == :statuses    )    plug(      OAuthScopesPlug,      %{scopes: ["read:accounts"]} -    when action in [:endorsements, :verify_credentials, :followers, :following] +    when action in [:verify_credentials, :endorsements, :identity_proofs]    )    plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials) @@ -58,21 +67,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships) -  # Note: :follows (POST /api/v1/follows) is the same as :follow, consider removing :follows    plug(      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", "write:mutes"]} when action in [:mute, :unmute]) -  plug( -    Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug -    when action not in [:create, :show, :statuses] -  ) -    @relationship_actions [:follow, :unfollow]    @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a @@ -378,7 +381,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    end    @doc "POST /api/v1/follows" -  def follows(%{body_params: %{uri: uri}} = conn, _) do +  def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do      case User.get_cached_by_nickname(uri) do        %User{} = user ->          conn diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex index 005c60444..408e11474 100644 --- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex @@ -5,6 +5,7 @@  defmodule Pleroma.Web.MastodonAPI.AppController do    use Pleroma.Web, :controller +  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.Repo    alias Pleroma.Web.OAuth.App @@ -13,7 +14,14 @@ defmodule Pleroma.Web.MastodonAPI.AppController do    action_fallback(Pleroma.Web.MastodonAPI.FallbackController) +  plug( +    :skip_plug, +    [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] +    when action == :create +  ) +    plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials) +    plug(OpenApiSpex.Plug.CastAndValidate)    @local_mastodon_name "Mastodon-Local" diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex index 37b389382..753b3db3e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -13,10 +13,10 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do    action_fallback(Pleroma.Web.MastodonAPI.FallbackController) -  @local_mastodon_name "Mastodon-Local" -    plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset) +  @local_mastodon_name "Mastodon-Local" +    @doc "GET /web/login"    def login(%{assigns: %{user: %User{}}} = conn, _params) do      redirect(conn, to: local_mastodon_root_path(conn)) diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex index 7c9b11bf1..c44641526 100644 --- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex @@ -14,9 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do    action_fallback(Pleroma.Web.MastodonAPI.FallbackController)    plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :index) -  plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action == :read) - -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) +  plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action != :index)    @doc "GET /api/v1/conversations"    def index(%{assigns: %{user: user}} = conn, params) do @@ -28,7 +26,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do    end    @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 <-             Repo.get_by(Participation, id: participation_id, user_id: user.id),           {:ok, participation} <- Participation.mark_as_read(participation) do diff --git a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex index 3bfebef8b..000ad743f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex @@ -7,6 +7,12 @@ defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do    plug(OpenApiSpex.Plug.CastAndValidate) +  plug( +    :skip_plug, +    [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] +    when action == :index +  ) +    defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.CustomEmojiOperation    def index(conn, _params) do diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex index 84de79413..c4fa383f2 100644 --- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex @@ -21,8 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do      %{scopes: ["follow", "write:blocks"]} when action != :index    ) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) -    @doc "GET /api/v1/domain_blocks"    def index(%{assigns: %{user: user}} = conn, _) do      json(conn, Map.get(user, :domain_blocks, [])) diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex index 7b0b937a2..7fd0562c9 100644 --- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex @@ -17,8 +17,6 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do      %{scopes: ["write:filters"]} when action not in @oauth_read_actions    ) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) -    @doc "GET /api/v1/filters"    def index(%{assigns: %{user: user}} = conn, _) do      filters = Filter.get_filters(user) diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex index 1ca86f63f..25f2269b9 100644 --- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex @@ -21,8 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do      %{scopes: ["follow", "write:follows"]} when action != :index    ) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) -    @doc "GET /api/v1/follow_requests"    def index(%{assigns: %{user: followed}} = conn, _params) do      follow_requests = User.get_follow_requests(followed) diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex index 27b5b1a52..237f85677 100644 --- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex @@ -5,6 +5,12 @@  defmodule Pleroma.Web.MastodonAPI.InstanceController do    use Pleroma.Web, :controller +  plug( +    :skip_plug, +    [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] +    when action in [:show, :peers] +  ) +    @doc "GET /api/v1/instance"    def show(conn, _params) do      render(conn, "show.json") diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex index dac4daa7b..bfe856025 100644 --- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex @@ -11,16 +11,16 @@ defmodule Pleroma.Web.MastodonAPI.ListController do    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(      OAuthScopesPlug,      %{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)    # GET /api/v1/lists diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex index 58e8a30c2..9f9d4574e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do    )    plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :upsert) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) +    action_fallback(Pleroma.Web.MastodonAPI.FallbackController)    # GET /api/v1/markers diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex index ac8c18f24..e7767de4e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -15,9 +15,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    require Logger -  plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug when action in [:empty_array, :empty_object]) - -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) +  plug( +    :skip_plug, +    [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] +    when action in [:empty_array, :empty_object] +  )    action_fallback(Pleroma.Web.MastodonAPI.FallbackController) diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 2b6f00952..e36751220 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -15,8 +15,6 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do    plug(OAuthScopesPlug, %{scopes: ["write:media"]}) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) -    @doc "POST /api/v1/media"    def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do      with {:ok, object} <- diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index 7fb536b09..311405277 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -20,8 +20,6 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do    plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) -    # GET /api/v1/notifications    def index(conn, %{"account_id" => account_id} = params) do      case Pleroma.User.get_cached_by_id(account_id) do diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex index d9f894118..af9b66eff 100644 --- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex @@ -22,8 +22,6 @@ defmodule Pleroma.Web.MastodonAPI.PollController do    plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :vote) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) -    @doc "GET /api/v1/polls/:id"    def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do      with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60), diff --git a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex index f5782be13..9fbaa7bd1 100644 --- a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex @@ -11,8 +11,6 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do    plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) -    @doc "POST /api/v1/reports"    def create(%{assigns: %{user: user}} = conn, params) do      with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex index e1e6bd89b..899b78873 100644 --- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex @@ -18,8 +18,6 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do    plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action 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)    @doc "GET /api/v1/scheduled_statuses" diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index c258742dd..cd49da6ad 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -21,7 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do    # Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)    plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated}) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) +  # Note: on private instances auth is required (EnsurePublicOrAuthenticatedPlug is not skipped)    plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search]) diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index f6e4f7d66..9eea2e9eb 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -24,6 +24,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do    alias Pleroma.Web.MastodonAPI.AccountView    alias Pleroma.Web.MastodonAPI.ScheduledActivityView +  plug(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show]) +    @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}    plug( @@ -77,8 +79,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do      %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]    ) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :show]) -    @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a    plug( @@ -358,7 +358,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do    end    @doc "GET /api/v1/favourites" -  def favourites(%{assigns: %{user: user}} = conn, params) do +  def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do      activities =        ActivityPub.fetch_favourites(          user, diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex index 4647c1f96..d184ea1d0 100644 --- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do    action_fallback(:errors)    plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]}) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) +    plug(:restrict_push_enabled)    # Creates PushSubscription diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 403d500e0..2d67e19da 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -9,11 +9,14 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do      only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1, skip_relationships?: 1]    alias Pleroma.Pagination +  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.Plugs.RateLimiter    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub +  plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:public, :hashtag]) +    # TODO: Replace with a macro when there is a Phoenix release with the following commit in it:    # https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e @@ -26,7 +29,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do    plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])    plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :public) +  plug( +    OAuthScopesPlug, +    %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} +    when action in [:public, :hashtag] +  )    plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) @@ -94,7 +101,9 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do      restrict? = Pleroma.Config.get([:restrict_unauthenticated, :timelines, cfg_key]) -    if not (restrict? and is_nil(user)) do +    if restrict? and is_nil(user) do +      render_error(conn, :unauthorized, "authorization required for timeline view") +    else        activities =          params          |> Map.put("type", ["Create", "Announce"]) @@ -112,12 +121,10 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do          as: :activity,          skip_relationships: skip_relationships?(params)        ) -    else -      render_error(conn, :unauthorized, "authorization required for timeline view")      end    end -  def hashtag_fetching(params, user, local_only) do +  defp hashtag_fetching(params, user, local_only) do      tags =        [params["tag"], params["any"]]        |> List.flatten() diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 1a09ac62a..4657a4383 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -4,6 +4,7 @@  defmodule Pleroma.Web.MediaProxy.MediaProxyController do    use Pleroma.Web, :controller +    alias Pleroma.ReverseProxy    alias Pleroma.Web.MediaProxy diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 0121cd661..685269877 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -25,9 +25,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do    plug(:fetch_session)    plug(:fetch_flash) -  plug(RateLimiter, [name: :authentication] when action == :create_authorization) -  plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug) +  plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]) + +  plug(RateLimiter, [name: :authentication] when action == :create_authorization)    action_fallback(Pleroma.Web.OAuth.FallbackController) diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 60405fbff..be7477867 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do      only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2, skip_relationships?: 1]    alias Ecto.Changeset +  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.Plugs.RateLimiter    alias Pleroma.User @@ -18,6 +19,11 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do    require Pleroma.Constants    plug( +    :skip_plug, +    [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirmation_resend +  ) + +  plug(      OAuthScopesPlug,      %{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]    ) @@ -33,15 +39,13 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do           ]    ) -  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 +    OAuthScopesPlug, +    %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites    )    plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend) +    plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])    plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index 03e95e020..e01825b48 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -1,6 +1,7 @@  defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do    use Pleroma.Web, :controller +  alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug    alias Pleroma.Plugs.OAuthScopesPlug    require Logger @@ -11,17 +12,20 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do      when action in [             :create,             :delete, -           :download_from, -           :list_from, +           :save_from,             :import_from_fs,             :update_file,             :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(        Pleroma.Config.get!([:instance, :static_dir]),        "emoji" @@ -212,13 +216,13 @@ keeping it in cache for #{div(cache_ms, 1000)}s")    end    @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`.    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.    """ -  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)      if shareable_packs_available(address) do diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex index d9c1c8636..d4e0d8b7c 100644 --- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@ -12,8 +12,6 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do    plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show)    plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) -    @doc "GET /api/v1/pleroma/mascot"    def show(%{assigns: %{user: user}} = conn, _params) do      json(conn, User.get_mascot(user)) diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex index fe1b97a20..2c1874051 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -28,18 +28,26 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do    plug(      OAuthScopesPlug, +    %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} +    when action == :emoji_reactions_by +  ) + +  plug( +    OAuthScopesPlug,      %{scopes: ["write:statuses"]}      when action in [:react_with_emoji, :unreact_with_emoji]    )    plug(      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(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) +  plug( +    OAuthScopesPlug, +    %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read +  )    def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id} = params) do      with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), @@ -167,7 +175,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do      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        conn        |> add_link_headers(participations) @@ -176,7 +184,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do      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        conn        |> put_view(NotificationView) @@ -189,7 +197,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do      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        notifications = Enum.take(notifications, 80) diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex index 4463ec477..22da6c0ad 100644 --- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex @@ -13,10 +13,12 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do    alias Pleroma.Web.CommonAPI    alias Pleroma.Web.MastodonAPI.StatusView -  plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :user_scrobbles) -  plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles) +  plug( +    OAuthScopesPlug, +    %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :user_scrobbles +  ) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) +  plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles)    def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do      params = diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 153802a43..becce3098 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -16,6 +16,14 @@ defmodule Pleroma.Web.Router do      plug(Pleroma.Plugs.UserEnabledPlug)    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      plug(Pleroma.Plugs.OAuthPlug)      plug(Pleroma.Plugs.BasicAuthDecoderPlug) @@ -39,20 +47,22 @@ defmodule Pleroma.Web.Router do    end    pipeline :api do +    plug(:expect_public_instance_or_authentication)      plug(:base_api)      plug(:after_auth)      plug(Pleroma.Plugs.IdempotencyPlug)    end    pipeline :authenticated_api do +    plug(:expect_authentication)      plug(:base_api) -    plug(Pleroma.Plugs.AuthExpectedPlug)      plug(:after_auth)      plug(Pleroma.Plugs.EnsureAuthenticatedPlug)      plug(Pleroma.Plugs.IdempotencyPlug)    end    pipeline :admin_api do +    plug(:expect_authentication)      plug(:base_api)      plug(Pleroma.Plugs.AdminSecretAuthenticationPlug)      plug(:after_auth) @@ -200,24 +210,28 @@ defmodule Pleroma.Web.Router do    end    scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do +    # Modifying packs      scope "/packs" do -      # Modifying packs        pipe_through(:admin_api)        post("/import_from_fs", EmojiAPIController, :import_from_fs) -        post("/:pack_name/update_file", EmojiAPIController, :update_file)        post("/:pack_name/update_metadata", EmojiAPIController, :update_metadata)        put("/:name", EmojiAPIController, :create)        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 +    # Pack info / downloading      scope "/packs" do -      # Pack info / downloading        get("/", EmojiAPIController, :list_packs)        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 @@ -277,7 +291,7 @@ defmodule Pleroma.Web.Router do        get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)        get("/conversations/:id", PleromaAPIController, :conversation) -      post("/conversations/read", PleromaAPIController, :read_conversations) +      post("/conversations/read", PleromaAPIController, :mark_conversations_as_read)      end      scope [] do @@ -286,7 +300,7 @@ defmodule Pleroma.Web.Router do        patch("/conversations/:id", PleromaAPIController, :update_conversation)        put("/statuses/:id/reactions/:emoji", PleromaAPIController, :react_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_banner", AccountController, :update_banner) @@ -322,53 +336,84 @@ defmodule Pleroma.Web.Router do      pipe_through(:authenticated_api)      get("/accounts/verify_credentials", AccountController, :verify_credentials) +    patch("/accounts/update_credentials", AccountController, :update_credentials)      get("/accounts/relationships", AccountController, :relationships) -      get("/accounts/:id/lists", AccountController, :lists)      get("/accounts/:id/identity_proofs", AccountController, :identity_proofs) - -    get("/follow_requests", FollowRequestController, :index) +    get("/endorsements", AccountController, :endorsements)      get("/blocks", AccountController, :blocks)      get("/mutes", AccountController, :mutes) -    get("/timelines/home", TimelineController, :home) -    get("/timelines/direct", TimelineController, :direct) +    post("/follows", AccountController, :follow_by_uri) +    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("/bookmarks", StatusController, :bookmarks) +    get("/apps/verify_credentials", AppController, :verify_credentials) + +    get("/conversations", ConversationController, :index) +    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/:id", NotificationController, :show) +      post("/notifications/:id/dismiss", NotificationController, :dismiss)      post("/notifications/clear", NotificationController, :clear)      delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)      # Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead      post("/notifications/dismiss", NotificationController, :dismiss) -    get("/scheduled_statuses", ScheduledActivityController, :index) -    get("/scheduled_statuses/:id", ScheduledActivityController, :show) - -    get("/lists", ListController, :index) -    get("/lists/:id", ListController, :show) -    get("/lists/:id/accounts", ListController, :list_accounts) - -    get("/domain_blocks", DomainBlockController, :index) - -    get("/filters", FilterController, :index) +    post("/polls/:id/votes", PollController, :vote) -    get("/suggestions", SuggestionController, :index) +    post("/reports", ReportController, :create) -    get("/conversations", ConversationController, :index) -    post("/conversations/:id/read", ConversationController, :read) +    get("/scheduled_statuses", ScheduledActivityController, :index) +    get("/scheduled_statuses/:id", ScheduledActivityController, :show) -    get("/endorsements", AccountController, :endorsements) +    put("/scheduled_statuses/:id", ScheduledActivityController, :update) +    delete("/scheduled_statuses/:id", ScheduledActivityController, :delete) -    patch("/accounts/update_credentials", AccountController, :update_credentials) +    # Unlike `GET /api/v1/accounts/:id/favourites`, demands authentication +    get("/favourites", StatusController, :favourites) +    get("/bookmarks", StatusController, :bookmarks)      post("/statuses", StatusController, :create)      delete("/statuses/:id", StatusController, :delete) -      post("/statuses/:id/reblog", StatusController, :reblog)      post("/statuses/:id/unreblog", StatusController, :unreblog)      post("/statuses/:id/favourite", StatusController, :favourite) @@ -380,49 +425,16 @@ defmodule Pleroma.Web.Router do      post("/statuses/:id/mute", StatusController, :mute_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)      get("/push/subscription", SubscriptionController, :get)      put("/push/subscription", SubscriptionController, :update)      delete("/push/subscription", SubscriptionController, :delete) -    get("/markers", MarkerController, :index) -    post("/markers", MarkerController, :upsert) +    get("/suggestions", SuggestionController, :index) + +    get("/timelines/home", TimelineController, :home) +    get("/timelines/direct", TimelineController, :direct) +    get("/timelines/list/:list_id", TimelineController, :list)    end    scope "/api/web", Pleroma.Web do @@ -434,15 +446,24 @@ defmodule Pleroma.Web.Router do    scope "/api/v1", Pleroma.Web.MastodonAPI do      pipe_through(:api) -    post("/accounts", AccountController, :create)      get("/accounts/search", SearchController, :account_search) +    get("/search", SearchController, :search) + +    get("/accounts/:id/statuses", AccountController, :statuses) +    get("/accounts/:id/followers", AccountController, :followers) +    get("/accounts/:id/following", AccountController, :following) +    get("/accounts/:id", AccountController, :show) + +    post("/accounts", AccountController, :create)      get("/instance", InstanceController, :show)      get("/instance/peers", InstanceController, :peers)      post("/apps", AppController, :create) -    get("/apps/verify_credentials", AppController, :verify_credentials) +    get("/statuses", StatusController, :index) +    get("/statuses/:id", StatusController, :show) +    get("/statuses/:id/context", StatusController, :context)      get("/statuses/:id/card", StatusController, :card)      get("/statuses/:id/favourited_by", StatusController, :favourited_by)      get("/statuses/:id/reblogged_by", StatusController, :reblogged_by) @@ -453,20 +474,8 @@ defmodule Pleroma.Web.Router do      get("/timelines/public", TimelineController, :public)      get("/timelines/tag/:tag", TimelineController, :hashtag) -    get("/timelines/list/:list_id", TimelineController, :list) - -    get("/statuses", StatusController, :index) -    get("/statuses/:id", StatusController, :show) -    get("/statuses/:id/context", StatusController, :context)      get("/polls/:id", PollController, :show) - -    get("/accounts/:id/statuses", AccountController, :statuses) -    get("/accounts/:id/followers", AccountController, :followers) -    get("/accounts/:id/following", AccountController, :following) -    get("/accounts/:id", AccountController, :show) - -    get("/search", SearchController, :search)    end    scope "/api/v2", Pleroma.Web.MastodonAPI do @@ -507,7 +516,11 @@ defmodule Pleroma.Web.Router do      get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)      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    pipeline :ostatus do @@ -647,11 +660,28 @@ defmodule Pleroma.Web.Router do    # Test-only routes needed to test action dispatching and plug chain execution    if Pleroma.Config.get(:env) == :test do +    @test_actions [ +      :do_oauth_check, +      :fallback_oauth_check, +      :skip_oauth_check, +      :fallback_oauth_skip_publicity_check, +      :skip_oauth_skip_publicity_check, +      :missing_oauth_check_definition +    ] + +    scope "/test/api", Pleroma.Tests do +      pipe_through(:api) + +      for action <- @test_actions do +        get("/#{action}", AuthTestController, action) +      end +    end +      scope "/test/authenticated_api", Pleroma.Tests do        pipe_through(:authenticated_api) -      for action <- [:skipped_oauth, :performed_oauth, :missed_oauth] do -        get("/#{action}", OAuthTestController, action) +      for action <- @test_actions do +        get("/#{action}", AuthTestController, action)        end      end    end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index d5d5ce08f..fd2aee175 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -25,13 +25,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do      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( diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 31adc2817..c2de26b0b 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    use Pleroma.Web, :controller    alias Pleroma.Notification +  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.User    alias Pleroma.Web.OAuth.Token @@ -13,11 +14,17 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    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, EnsurePublicOrAuthenticatedPlug] when action == :confirm_email +  ) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) +  plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token])    action_fallback(:errors) @@ -46,13 +53,13 @@ defmodule Pleroma.Web.TwitterAPI.Controller do      json_reply(conn, 201, "")    end -  def errors(conn, {:param_cast, _}) do +  defp errors(conn, {:param_cast, _}) do      conn      |> put_status(400)      |> json("Invalid parameters")    end -  def errors(conn, _) do +  defp errors(conn, _) do      conn      |> put_status(500)      |> json("Something went wrong") @@ -64,7 +71,10 @@ defmodule Pleroma.Web.TwitterAPI.Controller do      |> send_resp(status, json)    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)      notifications = Notification.for_user(user, params) @@ -75,7 +85,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do      |> render("index.json", %{notifications: notifications, for: user})    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")    end diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex index bf48ce26c..08e42a7e5 100644 --- a/lib/pleroma/web/web.ex +++ b/lib/pleroma/web/web.ex @@ -2,6 +2,11 @@  # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>  # 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    @moduledoc """    A module that keeps using definitions for controllers, @@ -20,44 +25,91 @@ defmodule Pleroma.Web do    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      quote do        use Phoenix.Controller, namespace: Pleroma.Web        import Plug.Conn +        import Pleroma.Web.Gettext        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.skip_plug(conn) -        rescue -          UndefinedFunctionError -> -            raise "#{plug_module} is not skippable. Append `use Pleroma.Web, :plug` to its code." -        end +      # Marks plugs intentionally skipped and blocks their execution if present in plugs chain +      defp skip_plug(conn, plug_modules) do +        plug_modules +        |> List.wrap() +        |> Enum.reduce( +          conn, +          fn plug_module, conn -> +            try do +              plug_module.skip_plug(conn) +            rescue +              UndefinedFunctionError -> +                raise "`#{plug_module}` is not skippable. Append `use Pleroma.Web, :plug` to its code." +            end +          end +        )        end        # Executed just before actual controller action, invokes before-action hooks (callbacks)        defp action(conn, params) do -        with %Plug.Conn{halted: false} <- maybe_halt_on_missing_oauth_scopes_check(conn) do +        with %{halted: false} = conn <- maybe_drop_authentication_if_oauth_check_ignored(conn), +             %{halted: false} = conn <- maybe_perform_public_or_authenticated_check(conn), +             %{halted: false} = conn <- maybe_perform_authenticated_check(conn), +             %{halted: false} = conn <- maybe_halt_on_missing_oauth_scopes_check(conn) do            super(conn, params)          end        end +      # For non-authenticated API actions, drops auth info if OAuth scopes check was ignored +      #   (neither performed nor explicitly skipped) +      defp maybe_drop_authentication_if_oauth_check_ignored(conn) do +        if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and +             not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do +          OAuthScopesPlug.drop_auth_info(conn) +        else +          conn +        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        defp maybe_halt_on_missing_oauth_scopes_check(conn) do -        if Pleroma.Plugs.AuthExpectedPlug.auth_expected?(conn) && -             not PlugHelper.plug_called_or_skipped?(conn, Pleroma.Plugs.OAuthScopesPlug) do +        if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and +             not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do            conn            |> render_error(              :forbidden, @@ -132,7 +184,8 @@ defmodule Pleroma.Web do    def plug do      quote do -      alias Pleroma.Plugs.PlugHelper +      @behaviour Pleroma.Web.Plug +      @behaviour Plug        @doc """        Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain. @@ -146,14 +199,22 @@ defmodule Pleroma.Web do        end        @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          if PlugHelper.plug_skipped?(conn, __MODULE__) do            conn          else -          conn -          |> PlugHelper.append_to_private_list(PlugHelper.called_plugs_list_id(), __MODULE__) -          |> perform(options) +          conn = +            PlugHelper.append_to_private_list( +              conn, +              PlugHelper.called_plugs_list_id(), +              __MODULE__ +            ) + +          apply(__MODULE__, :perform, [conn, options])          end        end      end  | 
