Merge branch 'develop' into 'remove-twitter-api'
[akkoma] / lib / pleroma / web / web.ex
index bfb6c728784055ab925799f1ef7f84de7aa0ee76..4f9281851dd5d43a2f3812d49cd89a7a509ef320 100644 (file)
@@ -1,7 +1,12 @@
 # Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# 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,11 +25,19 @@ 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
@@ -34,6 +47,79 @@ defmodule Pleroma.Web do
       defp set_put_layout(conn, _) do
         put_layout(conn, Pleroma.Config.get(:app_layout, "app.html"))
       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 %{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 PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and
+             not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
+          conn
+          |> render_error(
+            :forbidden,
+            "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
+          )
+          |> halt()
+        else
+          conn
+        end
+      end
     end
   end
 
@@ -66,23 +152,9 @@ defmodule Pleroma.Web do
       end
 
       @doc """
-      Same as `render_many/4` but wrapped in rescue block and parallelized (unless disabled by passing false as a fifth argument).
+      Same as `render_many/4` but wrapped in rescue block.
       """
-      def safe_render_many(collection, view, template, assigns \\ %{}, parallel \\ true)
-
-      def safe_render_many(collection, view, template, assigns, true) do
-        Enum.map(collection, fn resource ->
-          Task.async(fn ->
-            as = Map.get(assigns, :as) || view.__resource__
-            assigns = Map.put(assigns, as, resource)
-            safe_render(view, template, assigns)
-          end)
-        end)
-        |> Enum.map(&Task.await(&1, :infinity))
-        |> Enum.filter(& &1)
-      end
-
-      def safe_render_many(collection, view, template, assigns, false) do
+      def safe_render_many(collection, view, template, assigns \\ %{}) do
         Enum.map(collection, fn resource ->
           as = Map.get(assigns, :as) || view.__resource__
           assigns = Map.put(assigns, as, resource)
@@ -110,6 +182,50 @@ defmodule Pleroma.Web do
     end
   end
 
+  def plug do
+    quote do
+      @behaviour Pleroma.Web.Plug
+      @behaviour Plug
+
+      @doc """
+      Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain.
+      """
+      def skip_plug(conn) do
+        PlugHelper.append_to_private_list(
+          conn,
+          PlugHelper.skipped_plugs_list_id(),
+          __MODULE__
+        )
+      end
+
+      @impl Plug
+      @doc """
+      Before-plug hook that
+        * ensures the plug is not skipped
+        * processes `:if_func` / `:unless_func` functional pre-run conditions
+        * adds plug to the list of called plugs and calls `perform/2` if checks are passed
+
+      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__) ||
+             (options[:if_func] && !options[:if_func].(conn)) ||
+             (options[:unless_func] && options[:unless_func].(conn)) do
+          conn
+        else
+          conn =
+            PlugHelper.append_to_private_list(
+              conn,
+              PlugHelper.called_plugs_list_id(),
+              __MODULE__
+            )
+
+          apply(__MODULE__, :perform, [conn, options])
+        end
+      end
+    end
+  end
+
   @doc """
   When used, dispatch to the appropriate controller/view/etc.
   """