[#2349] Made :skip_plug/2 prevent plug from being executed even if explicitly called...
authorIvan Tashkinov <ivantashkinov@gmail.com>
Wed, 15 Apr 2020 18:19:16 +0000 (21:19 +0300)
committerIvan Tashkinov <ivantashkinov@gmail.com>
Wed, 15 Apr 2020 18:19:16 +0000 (21:19 +0300)
lib/pleroma/plugs/auth_expected_plug.ex
lib/pleroma/plugs/oauth_scopes_plug.ex
lib/pleroma/tests/oauth_test_controller.ex [new file with mode: 0644]
lib/pleroma/web/router.ex
lib/pleroma/web/web.ex
test/plugs/oauth_scopes_plug_test.exs
test/web/auth/oauth_test_controller_test.exs [new file with mode: 0644]

index 9e4a4bec850f6535dec3f425e5da7394f0274270..f79597dc35dd22706f94b6db5d98e299ba4be770 100644 (file)
@@ -10,4 +10,8 @@ defmodule Pleroma.Plugs.AuthExpectedPlug do
   def call(conn, _) do
     put_private(conn, :auth_expected, true)
   end
+
+  def auth_expected?(conn) do
+    conn.private[:auth_expected]
+  end
 end
index b09e1bb4dd118686fc7aa908525ea098c7495ef8..66f48c28c600f8fda50dea5c326edf47027125cc 100644 (file)
@@ -10,13 +10,13 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
   alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
   alias Pleroma.Plugs.PlugHelper
 
+  use Pleroma.Web, :plug
+
   @behaviour Plug
 
   def init(%{scopes: _} = options), do: options
 
-  def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
-    conn = PlugHelper.append_to_called_plugs(conn, __MODULE__)
-
+  def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
     op = options[:op] || :|
     token = assigns[:token]
 
diff --git a/lib/pleroma/tests/oauth_test_controller.ex b/lib/pleroma/tests/oauth_test_controller.ex
new file mode 100644 (file)
index 0000000..58d517f
--- /dev/null
@@ -0,0 +1,31 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+# A test controller reachable only in :test env.
+# Serves to test OAuth scopes check skipping / enforcement.
+defmodule Pleroma.Tests.OAuthTestController do
+  @moduledoc false
+
+  use Pleroma.Web, :controller
+
+  alias Pleroma.Plugs.OAuthScopesPlug
+
+  plug(:skip_plug, OAuthScopesPlug when action == :skipped_oauth)
+
+  plug(OAuthScopesPlug, %{scopes: ["read"]} when action != :missed_oauth)
+
+  def skipped_oauth(conn, _params) do
+    noop(conn)
+  end
+
+  def performed_oauth(conn, _params) do
+    noop(conn)
+  end
+
+  def missed_oauth(conn, _params) do
+    noop(conn)
+  end
+
+  defp noop(conn), do: json(conn, %{})
+end
index 8d13cd6c9333d9fbfbbef0f389c452211c7096c8..c85ad9f8bd581eff0e8169270ed246ba3aebd36a 100644 (file)
@@ -672,6 +672,17 @@ defmodule Pleroma.Web.Router do
     end
   end
 
+  # Test-only routes needed to test action dispatching and plug chain execution
+  if Pleroma.Config.get(:env) == :test do
+    scope "/test/authenticated_api", Pleroma.Tests do
+      pipe_through(:authenticated_api)
+
+      for action <- [:skipped_oauth, :performed_oauth, :missed_oauth] do
+        get("/#{action}", OAuthTestController, action)
+      end
+    end
+  end
+
   scope "/", Pleroma.Web.MongooseIM do
     get("/user_exists", MongooseIMController, :user_exists)
     get("/check_password", MongooseIMController, :check_password)
index 1af29ce788bcdadffb18871f372e1e76c256c252..ae7c94640f83065a91f3cc3e7a68d9540c879845 100644 (file)
@@ -37,15 +37,21 @@ defmodule Pleroma.Web do
         put_layout(conn, Pleroma.Config.get(:app_layout, "app.html"))
       end
 
-      # Marks a plug as intentionally skipped
-      #   (states that the plug is not called for a good reason, not by a mistake)
+      # Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain
       defp skip_plug(conn, plug_module) do
+        try do
+          plug_module.ensure_skippable()
+        rescue
+          UndefinedFunctionError ->
+            raise "#{plug_module} is not skippable. Append `use Pleroma.Web, :plug` to its code."
+        end
+
         PlugHelper.append_to_skipped_plugs(conn, plug_module)
       end
 
       # Here we can apply before-action hooks (e.g. verify whether auth checks were preformed)
       defp action(conn, params) do
-        if conn.private[:auth_expected] &&
+        if Pleroma.Plugs.AuthExpectedPlug.auth_expected?(conn) &&
              not PlugHelper.plug_called_or_skipped?(conn, Pleroma.Plugs.OAuthScopesPlug) do
           conn
           |> render_error(
@@ -119,6 +125,26 @@ defmodule Pleroma.Web do
     end
   end
 
+  def plug do
+    quote do
+      alias Pleroma.Plugs.PlugHelper
+
+      def ensure_skippable, do: :noop
+
+      @impl Plug
+      @doc "If marked as skipped, returns `conn`, and calls `perform/2` otherwise."
+      def call(%Plug.Conn{} = conn, options) do
+        if PlugHelper.plug_skipped?(conn, __MODULE__) do
+          conn
+        else
+          conn
+          |> PlugHelper.append_to_called_plugs(__MODULE__)
+          |> perform(options)
+        end
+      end
+    end
+  end
+
   @doc """
   When used, dispatch to the appropriate controller/view/etc.
   """
index e79ecf263842db21eb6d0915c2dc4a5f132d5aae..abab7abb0107960b206a52a9cc503d9bd89b3c97 100644 (file)
@@ -7,6 +7,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
 
   alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
   alias Pleroma.Plugs.OAuthScopesPlug
+  alias Pleroma.Plugs.PlugHelper
   alias Pleroma.Repo
 
   import Mock
@@ -16,6 +17,18 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
     :ok
   end
 
+  test "is not performed if marked as skipped", %{conn: conn} do
+    with_mock OAuthScopesPlug, [:passthrough], perform: &passthrough([&1, &2]) do
+      conn =
+        conn
+        |> PlugHelper.append_to_skipped_plugs(OAuthScopesPlug)
+        |> OAuthScopesPlug.call(%{scopes: ["random_scope"]})
+
+      refute called(OAuthScopesPlug.perform(:_, :_))
+      refute conn.halted
+    end
+  end
+
   test "if `token.scopes` fulfills specified 'any of' conditions, " <>
          "proceeds with no op",
        %{conn: conn} do
diff --git a/test/web/auth/oauth_test_controller_test.exs b/test/web/auth/oauth_test_controller_test.exs
new file mode 100644 (file)
index 0000000..a2f6009
--- /dev/null
@@ -0,0 +1,49 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Tests.OAuthTestControllerTest do
+  use Pleroma.Web.ConnCase
+
+  import Pleroma.Factory
+
+  setup %{conn: conn} do
+    user = insert(:user)
+    conn = assign(conn, :user, user)
+    %{conn: conn, user: user}
+  end
+
+  test "missed_oauth", %{conn: conn} do
+    res =
+      conn
+      |> get("/test/authenticated_api/missed_oauth")
+      |> json_response(403)
+
+    assert res ==
+             %{
+               "error" =>
+                 "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
+             }
+  end
+
+  test "skipped_oauth", %{conn: conn} do
+    conn
+    |> assign(:token, nil)
+    |> get("/test/authenticated_api/skipped_oauth")
+    |> json_response(200)
+  end
+
+  test "performed_oauth", %{user: user} do
+    %{conn: good_token_conn} = oauth_access(["read"], user: user)
+
+    good_token_conn
+    |> get("/test/authenticated_api/performed_oauth")
+    |> json_response(200)
+
+    %{conn: bad_token_conn} = oauth_access(["follow"], user: user)
+
+    bad_token_conn
+    |> get("/test/authenticated_api/performed_oauth")
+    |> json_response(403)
+  end
+end