[#1234] Mastodon 2.4.3 hierarchical scopes initial support (WIP).
authorIvan Tashkinov <ivantashkinov@gmail.com>
Sun, 8 Sep 2019 12:00:03 +0000 (15:00 +0300)
committerIvan Tashkinov <ivantashkinov@gmail.com>
Sun, 8 Sep 2019 12:00:03 +0000 (15:00 +0300)
lib/pleroma/plugs/oauth_scopes_plug.ex
lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
lib/pleroma/web/oauth/oauth_controller.ex
lib/pleroma/web/oauth/scopes.ex
lib/pleroma/web/router.ex
lib/pleroma/web/twitter_api/controllers/util_controller.ex
test/plugs/oauth_scopes_plug_test.exs
test/web/twitter_api/util_controller_test.exs

index b508628a92dca12b90e197c1acf3a3ca3c66b4bc..41403047efec8369fc8fbd011b1ac4b2b53c2922 100644 (file)
@@ -13,15 +13,16 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
   def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
     op = options[:op] || :|
     token = assigns[:token]
+    matched_scopes = token && filter_descendants(scopes, token.scopes)
 
     cond do
       is_nil(token) ->
         conn
 
-      op == :| && scopes -- token.scopes != scopes ->
+      op == :| && Enum.any?(matched_scopes) ->
         conn
 
-      op == :& && scopes -- token.scopes == [] ->
+      op == :& && matched_scopes == scopes ->
         conn
 
       options[:fallback] == :proceed_unauthenticated ->
@@ -30,7 +31,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
         |> assign(:token, nil)
 
       true ->
-        missing_scopes = scopes -- token.scopes
+        missing_scopes = scopes -- matched_scopes
         permissions = Enum.join(missing_scopes, " #{op} ")
 
         error_message =
@@ -42,4 +43,17 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
         |> halt()
     end
   end
+
+  @doc "Filters descendants of supported scopes"
+  def filter_descendants(scopes, supported_scopes) do
+    Enum.filter(
+      scopes,
+      fn scope ->
+        Enum.find(
+          supported_scopes,
+          &(scope == &1 || String.starts_with?(scope, &1 <> ":"))
+        )
+      end
+    )
+  end
 end
index 8dfad7a54e8d695c664508c6398e269b201e76e6..118446c8534ce4b5266e3cf0929eb9360b9d53b7 100644 (file)
@@ -19,6 +19,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.Pagination
+  alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.Plugs.RateLimiter
   alias Pleroma.Repo
   alias Pleroma.ScheduledActivity
@@ -52,6 +53,41 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   require Logger
   require Pleroma.Constants
 
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["follow", "read:blocks"]} when action in [:blocks, :domain_blocks]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["follow", "write:blocks"]}
+    when action in [:block, :unblock, :block_domain, :unblock_domain]
+  )
+
+  plug(OAuthScopesPlug, %{scopes: ["follow", "read:follows"]} when action == :follow_requests)
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["follow", "write:follows"]}
+    when action in [
+           :follow,
+           :unfollow,
+           :subscribe,
+           :unsubscribe,
+           :authorize_follow_request,
+           :reject_follow_request
+         ]
+  )
+
+  plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
+  plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:mutes"]}
+    when action in [:mute_conversation, :unmute_conversation]
+  )
+
   @rate_limited_relations_actions ~w(follow unfollow)a
 
   @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
index 81eae2c8be526a888f15abd58b517d099d2e166d..130ec78959055eac988225e77c1572dfb01334fa 100644 (file)
@@ -451,7 +451,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
   defp validate_scopes(app, params) do
     params
     |> Scopes.fetch_scopes(app.scopes)
-    |> Scopes.validates(app.scopes)
+    |> Scopes.validate(app.scopes)
   end
 
   def default_redirect_uri(%App{} = app) do
index ad9dfb2601b8fcee878247ef37bf58318ff4191d..48bd1440749340649309da78f20a7f36603a43bf 100644 (file)
@@ -8,7 +8,7 @@ defmodule Pleroma.Web.OAuth.Scopes do
   """
 
   @doc """
-  Fetch scopes from requiest params.
+  Fetch scopes from request params.
 
   Note: `scopes` is used by Mastodon — supporting it but sticking to
   OAuth's standard `scope` wherever we control it
@@ -53,14 +53,14 @@ defmodule Pleroma.Web.OAuth.Scopes do
   @doc """
   Validates scopes.
   """
-  @spec validates(list() | nil, list()) ::
+  @spec validate(list() | nil, list()) ::
           {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
-  def validates([], _app_scopes), do: {:error, :missing_scopes}
-  def validates(nil, _app_scopes), do: {:error, :missing_scopes}
+  def validate([], _app_scopes), do: {:error, :missing_scopes}
+  def validate(nil, _app_scopes), do: {:error, :missing_scopes}
 
-  def validates(scopes, app_scopes) do
-    case scopes -- app_scopes do
-      [] -> {:ok, scopes}
+  def validate(scopes, app_scopes) do
+    case Pleroma.Plugs.OAuthScopesPlug.filter_descendants(scopes, app_scopes) do
+      ^scopes -> {:ok, scopes}
       _ -> {:error, :unsupported_scopes}
     end
   end
index cfb973f532def19d8b81029763db4015dad20ed0..8c93e535e0e67c402e2c01fc6a09dd9347aee1f4 100644 (file)
@@ -104,10 +104,6 @@ defmodule Pleroma.Web.Router do
     plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["write"]})
   end
 
-  pipeline :oauth_follow do
-    plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["follow"]})
-  end
-
   pipeline :oauth_push do
     plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
   end
@@ -211,11 +207,7 @@ defmodule Pleroma.Web.Router do
 
     post("/main/ostatus", UtilController, :remote_subscribe)
     get("/ostatus_subscribe", UtilController, :remote_follow)
-
-    scope [] do
-      pipe_through(:oauth_follow)
-      post("/ostatus_subscribe", UtilController, :do_remote_follow)
-    end
+    post("/ostatus_subscribe", UtilController, :do_remote_follow)
   end
 
   scope "/api/pleroma", Pleroma.Web.TwitterAPI do
@@ -231,8 +223,6 @@ defmodule Pleroma.Web.Router do
     end
 
     scope [] do
-      pipe_through(:oauth_follow)
-
       post("/blocks_import", UtilController, :blocks_import)
       post("/follow_import", UtilController, :follow_import)
     end
@@ -373,8 +363,6 @@ defmodule Pleroma.Web.Router do
     end
 
     scope [] do
-      pipe_through(:oauth_follow)
-
       post("/follows", MastodonAPIController, :follow)
       post("/accounts/:id/follow", MastodonAPIController, :follow)
 
index 3405bd3b7f5c2ab5d551183ec7fd32666d76f9a9..1c6ad5057551a91dc087ac86a7c2cfece5f93615 100644 (file)
@@ -13,11 +13,20 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
   alias Pleroma.Healthcheck
   alias Pleroma.Notification
   alias Pleroma.Plugs.AuthenticationPlug
+  alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.User
   alias Pleroma.Web
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.WebFinger
 
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["follow", "write:follows"]}
+    when action in [:do_remote_follow, :follow_import]
+  )
+
+  plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks_import)
+
   plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version])
 
   def help_test(conn, _params) do
index f328026dfac5cc90298e46c41851db0bc923670d..9b0a2e7025af7a2690314f19566874e0c65be0ee 100644 (file)
@@ -84,7 +84,8 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
     refute conn.assigns[:user]
   end
 
-  test "returns 403 and halts in case of no :fallback option and `token.scopes` not fulfilling specified 'any of' conditions",
+  test "returns 403 and halts " <>
+         "in case of no :fallback option and `token.scopes` not fulfilling specified 'any of' conditions",
        %{conn: conn} do
     token = insert(:oauth_token, scopes: ["read", "write"])
     any_of_scopes = ["follow"]
@@ -101,7 +102,8 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
     assert Jason.encode!(%{error: expected_error}) == conn.resp_body
   end
 
-  test "returns 403 and halts in case of no :fallback option and `token.scopes` not fulfilling specified 'all of' conditions",
+  test "returns 403 and halts " <>
+         "in case of no :fallback option and `token.scopes` not fulfilling specified 'all of' conditions",
        %{conn: conn} do
     token = insert(:oauth_token, scopes: ["read", "write"])
     all_of_scopes = ["write", "follow"]
@@ -119,4 +121,36 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
 
     assert Jason.encode!(%{error: expected_error}) == conn.resp_body
   end
+
+  describe "with hierarchical scopes, " do
+    test "proceeds with no op if `token.scopes` fulfill specified 'any of' conditions", %{
+      conn: conn
+    } do
+      token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
+
+      conn =
+        conn
+        |> assign(:user, token.user)
+        |> assign(:token, token)
+        |> OAuthScopesPlug.call(%{scopes: ["read:something"]})
+
+      refute conn.halted
+      assert conn.assigns[:user]
+    end
+
+    test "proceeds with no op if `token.scopes` fulfill specified 'all of' conditions", %{
+      conn: conn
+    } do
+      token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user)
+
+      conn =
+        conn
+        |> assign(:user, token.user)
+        |> assign(:token, token)
+        |> OAuthScopesPlug.call(%{scopes: ["scope1:subscope", "scope2:subscope"], op: :&})
+
+      refute conn.halted
+      assert conn.assigns[:user]
+    end
+  end
 end
index cf8e69d2b4e61f46af7e140164bf9d2631d8d8b2..685e482708ab631fed713a92bbf9dce8fde3d2c8 100644 (file)
@@ -78,19 +78,21 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
       assert response == "job started"
     end
 
-    test "requires 'follow' permission", %{conn: conn} do
+    test "requires 'follow' or 'write:follows' permissions", %{conn: conn} do
       token1 = insert(:oauth_token, scopes: ["read", "write"])
       token2 = insert(:oauth_token, scopes: ["follow"])
+      token3 = insert(:oauth_token, scopes: ["something"])
       another_user = insert(:user)
 
-      for token <- [token1, token2] do
+      for token <- [token1, token2, token3] do
         conn =
           conn
           |> put_req_header("authorization", "Bearer #{token.token}")
           |> post("/api/pleroma/follow_import", %{"list" => "#{another_user.ap_id}"})
 
-        if token == token1 do
-          assert %{"error" => "Insufficient permissions: follow."} == json_response(conn, 403)
+        if token == token3 do
+          assert %{"error" => "Insufficient permissions: follow | write:follows."} ==
+                   json_response(conn, 403)
         else
           assert json_response(conn, 200)
         end