Merge remote-tracking branch 'remotes/upstream/develop' into 1234-mastodon-2-4-3...
authorIvan Tashkinov <ivantashkinov@gmail.com>
Wed, 18 Sep 2019 09:32:02 +0000 (12:32 +0300)
committerIvan Tashkinov <ivantashkinov@gmail.com>
Wed, 18 Sep 2019 09:32:02 +0000 (12:32 +0300)
19 files changed:
CHANGELOG.md
lib/pleroma/plugs/oauth_scopes_plug.ex
lib/pleroma/web/activity_pub/activity_pub_controller.ex
lib/pleroma/web/admin_api/admin_api_controller.ex
lib/pleroma/web/mastodon_api/controllers/list_controller.ex
lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
lib/pleroma/web/mastodon_api/controllers/search_controller.ex
lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
lib/pleroma/web/oauth/oauth_controller.ex
lib/pleroma/web/oauth/scopes.ex
lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
lib/pleroma/web/router.ex
lib/pleroma/web/twitter_api/controllers/util_controller.ex
lib/pleroma/web/twitter_api/twitter_api_controller.ex
test/plugs/oauth_scopes_plug_test.exs
test/support/factory.ex
test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs
test/web/oauth/oauth_controller_test.exs
test/web/twitter_api/util_controller_test.exs

index f3f38b817f1c09e9b065e862ecafaf80593c1c5e..dcf69aeb6b634f7f1d0e0aacb19ae791a5ee3566 100644 (file)
@@ -95,6 +95,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Admin API: Added moderation log
 - Web response cache (currently, enabled for ActivityPub)
 - Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)
+- OAuth: support for hierarchical permissions / [Mastodon 2.4.3 OAuth permissions](https://docs.joinmastodon.org/api/permissions/)
 
 ### Changed
 - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
index b508628a92dca12b90e197c1acf3a3ca3c66b4bc..e0d61c4ebebef059954a334b57c08f367bce2918 100644 (file)
@@ -6,6 +6,8 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
   import Plug.Conn
   import Pleroma.Web.Gettext
 
+  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
+
   @behaviour Plug
 
   def init(%{scopes: _} = options), do: options
@@ -13,24 +15,26 @@ 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
+        maybe_perform_instance_privacy_check(conn, options)
 
-      op == :| && scopes -- token.scopes != scopes ->
+      op == :| && Enum.any?(matched_scopes) ->
         conn
 
-      op == :& && scopes -- token.scopes == [] ->
+      op == :& && matched_scopes == scopes ->
         conn
 
       options[:fallback] == :proceed_unauthenticated ->
         conn
         |> assign(:user, nil)
         |> assign(:token, nil)
+        |> maybe_perform_instance_privacy_check(options)
 
       true ->
-        missing_scopes = scopes -- token.scopes
+        missing_scopes = scopes -- matched_scopes
         permissions = Enum.join(missing_scopes, " #{op} ")
 
         error_message =
@@ -42,4 +46,24 @@ 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
+
+  defp maybe_perform_instance_privacy_check(%Plug.Conn{} = conn, options) do
+    case options[:skip_instance_privacy_check] do
+      true -> conn
+      _ -> EnsurePublicOrAuthenticatedPlug.call(conn, [])
+    end
+  end
 end
index 01b34fb1d475df01dc3d2ce5f87f762e359360e8..5ea749141f60664428efdfe2ee1fea4a84f30548 100644 (file)
@@ -30,6 +30,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
     when action in [:activity, :object]
   )
 
+  plug(
+    Pleroma.Plugs.OAuthScopesPlug,
+    %{scopes: ["read:accounts"]} when action in [:followers, :following]
+  )
+
   plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
   plug(:set_requester_reachable when action in [:inbox])
   plug(:relay_active? when action in [:relay])
index 2a1cc59e55d69f3f7098b5242bf9f0b86883fe30..7f1a8e5662a7d759662e6c20805d7d61bc55f691 100644 (file)
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
   use Pleroma.Web, :controller
   alias Pleroma.Activity
   alias Pleroma.ModerationLog
+  alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.User
   alias Pleroma.UserInviteToken
   alias Pleroma.Web.ActivityPub.ActivityPub
@@ -23,6 +24,67 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
 
   require Logger
 
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read:accounts"]}
+    when action in [:list_users, :user_show, :right_get, :invites]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:accounts"]}
+    when action in [
+           :get_invite_token,
+           :revoke_invite,
+           :email_invite,
+           :get_password_reset,
+           :user_follow,
+           :user_unfollow,
+           :user_delete,
+           :users_create,
+           :user_toggle_activation,
+           :tag_users,
+           :untag_users,
+           :right_add,
+           :right_delete,
+           :set_activation_status
+         ]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read:reports"]} when action in [:list_reports, :report_show]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:reports"]}
+    when action in [:report_update_state, :report_respond]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read:statuses"]} when action == :list_user_statuses
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:statuses"]}
+    when action in [:status_update, :status_delete]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read"]}
+    when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write"]}
+    when action in [:relay_follow, :relay_unfollow, :config_update]
+  )
+
   @users_page_size 50
 
   action_fallback(:errors)
@@ -400,7 +462,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
     end
   end
 
-  @doc "Get a account registeration invite token (base64 string)"
+  @doc "Get a account registration invite token (base64 string)"
   def get_invite_token(conn, params) do
     options = params["invite"] || %{}
     {:ok, invite} = UserInviteToken.create_invite(options)
index 2873deda8ddc891fe6be836c5063bb90560480de..be70896305f165a81a84190db0b49f9f828198de 100644 (file)
@@ -5,11 +5,20 @@
 defmodule Pleroma.Web.MastodonAPI.ListController do
   use Pleroma.Web, :controller
 
+  alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.User
   alias Pleroma.Web.MastodonAPI.AccountView
 
   plug(:list_by_id_and_user when action not in [:index, :create])
 
+  plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in [:index, :show, :list_accounts])
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:lists"]}
+    when action in [:create, :update, :delete, :add_to_list, :remove_from_list]
+  )
+
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
 
   # GET /api/v1/lists
index 060137b80e38fe45945c6d8d34892e3830f86c0b..d7a83a2f5158fd5be9f11cac872015e891840a8b 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,190 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   require Logger
   require Pleroma.Constants
 
+  @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
+
+  # Note: :index action handles attempt of unauthenticated access to private instance with redirect
+  plug(
+    OAuthScopesPlug,
+    Map.merge(@unauthenticated_access, %{scopes: ["read"], skip_instance_privacy_check: true})
+    when action == :index
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read"]} when action in [:suggestions, :verify_app_credentials]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:accounts"]}
+    # Note: the following actions are not permission-secured in Mastodon:
+    when action in [
+           :put_settings,
+           :update_avatar,
+           :update_banner,
+           :update_background,
+           :set_mascot
+         ]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:accounts"]}
+    when action in [:pin_status, :unpin_status, :update_credentials]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read:statuses"]}
+    when action in [
+           :conversations,
+           :scheduled_statuses,
+           :show_scheduled_status,
+           :home_timeline,
+           :dm_timeline
+         ]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{@unauthenticated_access | scopes: ["read:statuses"]}
+    when action in [
+           :user_statuses,
+           :get_statuses,
+           :get_status,
+           :get_context,
+           :status_card,
+           :get_poll
+         ]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:statuses"]}
+    when action in [
+           :update_scheduled_status,
+           :delete_scheduled_status,
+           :post_status,
+           :delete_status,
+           :reblog_status,
+           :unreblog_status,
+           :poll_vote
+         ]
+  )
+
+  plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action == :conversation_read)
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read:accounts"]}
+    when action in [:endorsements, :verify_credentials, :followers, :following, :get_mascot]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{@unauthenticated_access | scopes: ["read:accounts"]}
+    when action in [:user, :favourited_by, :reblogged_by]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read:favourites"]} when action in [:favourites, :user_favourites]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:favourites"]} when action in [:fav_status, :unfav_status]
+  )
+
+  plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in [:get_filters, :get_filter])
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:filters"]} when action in [:create_filter, :update_filter, :delete_filter]
+  )
+
+  plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in [:account_lists, :list_timeline])
+
+  plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action in [:upload, :update_media])
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read:notifications"]} when action in [:notifications, :get_notification]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:notifications"]}
+    when action in [:clear_notifications, :dismiss_notification, :destroy_multiple_notifications]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:reports"]}
+    when action in [:create_report, :report_update_state, :report_respond]
+  )
+
+  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: ["read:follows"]} when action == :relationships)
+  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]
+  )
+
+  # Note: scopes not present in Mastodon: read:bookmarks, write:bookmarks
+  plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:bookmarks"]} when action in [:bookmark_status, :unbookmark_status]
+  )
+
+  # An extra safety measure for possible actions not guarded by OAuth permissions specification
+  plug(
+    Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
+    when action not in [
+           :account_register,
+           :create_app,
+           :index,
+           :login,
+           :logout,
+           :password_reset,
+           :account_confirmation_resend,
+           :masto_instance,
+           :peers,
+           :custom_emojis
+         ]
+  )
+
   @rate_limited_relations_actions ~w(follow unfollow)a
 
   @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
@@ -754,7 +939,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
-  def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
+  def destroy_multiple_notifications(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
     Notification.destroy_multiple(user, ids)
     json(conn, %{})
   end
@@ -1470,6 +1655,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     json(conn, %{})
   end
 
+  def endorsements(conn, params), do: empty_array(conn, params)
+
   def get_filters(%{assigns: %{user: user}} = conn, _) do
     filters = Filter.get_filters(user)
     res = FilterView.render("filters.json", filters: filters)
@@ -1592,7 +1779,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
-  def reports(%{assigns: %{user: user}} = conn, params) do
+  def create_report(%{assigns: %{user: user}} = conn, params) do
     case CommonAPI.report(user, params) do
       {:ok, activity} ->
         conn
index 9072aa7a47c1a3702b8ae46b45cfd676be57d30a..f49ca89edfe603fed6f031178a1e05c7d833a983 100644 (file)
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
   use Pleroma.Web, :controller
 
   alias Pleroma.Activity
+  alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.Plugs.RateLimiter
   alias Pleroma.Repo
   alias Pleroma.User
@@ -15,6 +16,10 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
   alias Pleroma.Web.MastodonAPI.StatusView
 
   require Logger
+
+  # Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
+  plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated})
+
   plug(RateLimiter, :search when action in [:search, :search2, :account_search])
 
   def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
index e2b17aab1ebd7e7bc894bc65f0d6a11f4203ad08..287eebf921e4b726b6164fb2a21f54c71bc35e7a 100644 (file)
@@ -12,6 +12,8 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
 
   action_fallback(:errors)
 
+  plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
+
   # Creates PushSubscription
   # POST /api/v1/push/subscription
   #
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 d17ccf84d0778ff5de3972f365a8a5df12e12f76..f3dc4616cd7a05b8bd193ff4b28639c7380b5d66 100644 (file)
@@ -9,11 +9,24 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
 
   alias Pleroma.Conversation.Participation
   alias Pleroma.Notification
+  alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.MastodonAPI.ConversationView
   alias Pleroma.Web.MastodonAPI.NotificationView
   alias Pleroma.Web.MastodonAPI.StatusView
 
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read:statuses"]} when action in [:conversation, :conversation_statuses]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:conversations"]} when action in [:conversations, :conversation_read]
+  )
+
+  plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification)
+
   def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
     with %Participation{} = participation <- Participation.get(participation_id),
          true <- user.id == participation.user_id do
index 401133bf3d750c4a55adaf3dafce2b09467bad9b..8448a00a18eda29a3595561e0cb0d3f08bd56435 100644 (file)
@@ -87,31 +87,6 @@ defmodule Pleroma.Web.Router do
     plug(Pleroma.Plugs.EnsureUserKeyPlug)
   end
 
-  pipeline :oauth_read_or_public do
-    plug(Pleroma.Plugs.OAuthScopesPlug, %{
-      scopes: ["read"],
-      fallback: :proceed_unauthenticated
-    })
-
-    plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-  end
-
-  pipeline :oauth_read do
-    plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["read"]})
-  end
-
-  pipeline :oauth_write 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
-
   pipeline :well_known do
     plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"])
   end
@@ -154,7 +129,7 @@ defmodule Pleroma.Web.Router do
   end
 
   scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
-    pipe_through([:admin_api, :oauth_write])
+    pipe_through(:admin_api)
 
     post("/users/follow", AdminAPIController, :user_follow)
     post("/users/unfollow", AdminAPIController, :user_unfollow)
@@ -212,32 +187,20 @@ 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
     pipe_through(:authenticated_api)
 
-    scope [] do
-      pipe_through(:oauth_write)
-
-      post("/change_email", UtilController, :change_email)
-      post("/change_password", UtilController, :change_password)
-      post("/delete_account", UtilController, :delete_account)
-      put("/notification_settings", UtilController, :update_notificaton_settings)
-      post("/disable_account", UtilController, :disable_account)
-    end
+    post("/change_email", UtilController, :change_email)
+    post("/change_password", UtilController, :change_password)
+    post("/delete_account", UtilController, :delete_account)
+    put("/notification_settings", UtilController, :update_notificaton_settings)
+    post("/disable_account", UtilController, :disable_account)
 
-    scope [] do
-      pipe_through(:oauth_follow)
-
-      post("/blocks_import", UtilController, :blocks_import)
-      post("/follow_import", UtilController, :follow_import)
-    end
+    post("/blocks_import", UtilController, :blocks_import)
+    post("/follow_import", UtilController, :follow_import)
   end
 
   scope "/oauth", Pleroma.Web.OAuth do
@@ -264,150 +227,136 @@ defmodule Pleroma.Web.Router do
   scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
     pipe_through(:authenticated_api)
 
-    scope [] do
-      pipe_through(:oauth_read)
-      get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
-      get("/conversations/:id", PleromaAPIController, :conversation)
-    end
+    get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
+    get("/conversations/:id", PleromaAPIController, :conversation)
 
-    scope [] do
-      pipe_through(:oauth_write)
-      patch("/conversations/:id", PleromaAPIController, :update_conversation)
-      post("/notifications/read", PleromaAPIController, :read_notification)
-    end
+    patch("/conversations/:id", PleromaAPIController, :update_conversation)
+    post("/notifications/read", PleromaAPIController, :read_notification)
   end
 
   scope "/api/v1", Pleroma.Web.MastodonAPI do
     pipe_through(:authenticated_api)
 
-    scope [] do
-      pipe_through(:oauth_read)
-
-      get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials)
-
-      get("/accounts/relationships", MastodonAPIController, :relationships)
-
-      get("/accounts/:id/lists", MastodonAPIController, :account_lists)
-      get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
+    get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials)
 
-      get("/follow_requests", MastodonAPIController, :follow_requests)
-      get("/blocks", MastodonAPIController, :blocks)
-      get("/mutes", MastodonAPIController, :mutes)
+    get("/accounts/relationships", MastodonAPIController, :relationships)
 
-      get("/timelines/home", MastodonAPIController, :home_timeline)
-      get("/timelines/direct", MastodonAPIController, :dm_timeline)
+    get("/accounts/:id/lists", MastodonAPIController, :account_lists)
+    get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
 
-      get("/favourites", MastodonAPIController, :favourites)
-      get("/bookmarks", MastodonAPIController, :bookmarks)
+    get("/follow_requests", MastodonAPIController, :follow_requests)
+    get("/blocks", MastodonAPIController, :blocks)
+    get("/mutes", MastodonAPIController, :mutes)
 
-      post("/notifications/clear", MastodonAPIController, :clear_notifications)
-      post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
-      get("/notifications", MastodonAPIController, :notifications)
-      get("/notifications/:id", MastodonAPIController, :get_notification)
-      delete("/notifications/destroy_multiple", MastodonAPIController, :destroy_multiple)
+    get("/timelines/home", MastodonAPIController, :home_timeline)
+    get("/timelines/direct", MastodonAPIController, :dm_timeline)
 
-      get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses)
-      get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status)
+    get("/favourites", MastodonAPIController, :favourites)
+    # Note: not present in Mastodon: bookmarks
+    get("/bookmarks", MastodonAPIController, :bookmarks)
 
-      get("/lists", ListController, :index)
-      get("/lists/:id", ListController, :show)
-      get("/lists/:id/accounts", ListController, :list_accounts)
+    post("/notifications/clear", MastodonAPIController, :clear_notifications)
+    post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
+    get("/notifications", MastodonAPIController, :notifications)
+    get("/notifications/:id", MastodonAPIController, :get_notification)
 
-      get("/domain_blocks", MastodonAPIController, :domain_blocks)
+    delete(
+      "/notifications/destroy_multiple",
+      MastodonAPIController,
+      :destroy_multiple_notifications
+    )
 
-      get("/filters", MastodonAPIController, :get_filters)
+    get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses)
+    get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status)
 
-      get("/suggestions", MastodonAPIController, :suggestions)
+    get("/lists", ListController, :index)
+    get("/lists/:id", ListController, :show)
+    get("/lists/:id/accounts", ListController, :list_accounts)
 
-      get("/conversations", MastodonAPIController, :conversations)
-      post("/conversations/:id/read", MastodonAPIController, :conversation_read)
+    get("/domain_blocks", MastodonAPIController, :domain_blocks)
 
-      get("/endorsements", MastodonAPIController, :empty_array)
-    end
+    get("/filters", MastodonAPIController, :get_filters)
 
-    scope [] do
-      pipe_through(:oauth_write)
+    get("/suggestions", MastodonAPIController, :suggestions)
 
-      patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
+    get("/conversations", MastodonAPIController, :conversations)
+    post("/conversations/:id/read", MastodonAPIController, :conversation_read)
 
-      post("/statuses", MastodonAPIController, :post_status)
-      delete("/statuses/:id", MastodonAPIController, :delete_status)
+    get("/endorsements", MastodonAPIController, :endorsements)
 
-      post("/statuses/:id/reblog", MastodonAPIController, :reblog_status)
-      post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status)
-      post("/statuses/:id/favourite", MastodonAPIController, :fav_status)
-      post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status)
-      post("/statuses/:id/pin", MastodonAPIController, :pin_status)
-      post("/statuses/:id/unpin", MastodonAPIController, :unpin_status)
-      post("/statuses/:id/bookmark", MastodonAPIController, :bookmark_status)
-      post("/statuses/:id/unbookmark", MastodonAPIController, :unbookmark_status)
-      post("/statuses/:id/mute", MastodonAPIController, :mute_conversation)
-      post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation)
+    patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
 
-      put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)
-      delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status)
+    post("/statuses", MastodonAPIController, :post_status)
+    delete("/statuses/:id", MastodonAPIController, :delete_status)
 
-      post("/polls/:id/votes", MastodonAPIController, :poll_vote)
+    post("/statuses/:id/reblog", MastodonAPIController, :reblog_status)
+    post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status)
+    post("/statuses/:id/favourite", MastodonAPIController, :fav_status)
+    post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status)
+    post("/statuses/:id/pin", MastodonAPIController, :pin_status)
+    post("/statuses/:id/unpin", MastodonAPIController, :unpin_status)
+    # Note: not present in Mastodon: bookmark
+    post("/statuses/:id/bookmark", MastodonAPIController, :bookmark_status)
+    # Note: not present in Mastodon: unbookmark
+    post("/statuses/:id/unbookmark", MastodonAPIController, :unbookmark_status)
+    post("/statuses/:id/mute", MastodonAPIController, :mute_conversation)
+    post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation)
 
-      post("/media", MastodonAPIController, :upload)
-      put("/media/:id", MastodonAPIController, :update_media)
+    put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)
+    delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status)
 
-      delete("/lists/:id", ListController, :delete)
-      post("/lists", ListController, :create)
-      put("/lists/:id", ListController, :update)
+    post("/polls/:id/votes", MastodonAPIController, :poll_vote)
 
-      post("/lists/:id/accounts", ListController, :add_to_list)
-      delete("/lists/:id/accounts", ListController, :remove_from_list)
+    post("/media", MastodonAPIController, :upload)
+    put("/media/:id", MastodonAPIController, :update_media)
 
-      post("/filters", MastodonAPIController, :create_filter)
-      get("/filters/:id", MastodonAPIController, :get_filter)
-      put("/filters/:id", MastodonAPIController, :update_filter)
-      delete("/filters/:id", MastodonAPIController, :delete_filter)
+    delete("/lists/:id", ListController, :delete)
+    post("/lists", ListController, :create)
+    put("/lists/:id", ListController, :update)
 
-      patch("/pleroma/accounts/update_avatar", MastodonAPIController, :update_avatar)
-      patch("/pleroma/accounts/update_banner", MastodonAPIController, :update_banner)
-      patch("/pleroma/accounts/update_background", MastodonAPIController, :update_background)
+    post("/lists/:id/accounts", ListController, :add_to_list)
+    delete("/lists/:id/accounts", ListController, :remove_from_list)
 
-      get("/pleroma/mascot", MastodonAPIController, :get_mascot)
-      put("/pleroma/mascot", MastodonAPIController, :set_mascot)
+    post("/filters", MastodonAPIController, :create_filter)
+    get("/filters/:id", MastodonAPIController, :get_filter)
+    put("/filters/:id", MastodonAPIController, :update_filter)
+    delete("/filters/:id", MastodonAPIController, :delete_filter)
 
-      post("/reports", MastodonAPIController, :reports)
-    end
+    patch("/pleroma/accounts/update_avatar", MastodonAPIController, :update_avatar)
+    patch("/pleroma/accounts/update_banner", MastodonAPIController, :update_banner)
+    patch("/pleroma/accounts/update_background", MastodonAPIController, :update_background)
 
-    scope [] do
-      pipe_through(:oauth_follow)
+    get("/pleroma/mascot", MastodonAPIController, :get_mascot)
+    put("/pleroma/mascot", MastodonAPIController, :set_mascot)
 
-      post("/follows", MastodonAPIController, :follow)
-      post("/accounts/:id/follow", MastodonAPIController, :follow)
+    post("/reports", MastodonAPIController, :create_report)
 
-      post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
-      post("/accounts/:id/block", MastodonAPIController, :block)
-      post("/accounts/:id/unblock", MastodonAPIController, :unblock)
-      post("/accounts/:id/mute", MastodonAPIController, :mute)
-      post("/accounts/:id/unmute", MastodonAPIController, :unmute)
+    post("/follows", MastodonAPIController, :follow)
+    post("/accounts/:id/follow", MastodonAPIController, :follow)
 
-      post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
-      post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request)
+    post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
+    post("/accounts/:id/block", MastodonAPIController, :block)
+    post("/accounts/:id/unblock", MastodonAPIController, :unblock)
+    post("/accounts/:id/mute", MastodonAPIController, :mute)
+    post("/accounts/:id/unmute", MastodonAPIController, :unmute)
 
-      post("/domain_blocks", MastodonAPIController, :block_domain)
-      delete("/domain_blocks", MastodonAPIController, :unblock_domain)
+    post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
+    post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request)
 
-      post("/pleroma/accounts/:id/subscribe", MastodonAPIController, :subscribe)
-      post("/pleroma/accounts/:id/unsubscribe", MastodonAPIController, :unsubscribe)
-    end
+    post("/domain_blocks", MastodonAPIController, :block_domain)
+    delete("/domain_blocks", MastodonAPIController, :unblock_domain)
 
-    scope [] do
-      pipe_through(:oauth_push)
+    post("/pleroma/accounts/:id/subscribe", MastodonAPIController, :subscribe)
+    post("/pleroma/accounts/:id/unsubscribe", MastodonAPIController, :unsubscribe)
 
-      post("/push/subscription", SubscriptionController, :create)
-      get("/push/subscription", SubscriptionController, :get)
-      put("/push/subscription", SubscriptionController, :update)
-      delete("/push/subscription", SubscriptionController, :delete)
-    end
+    post("/push/subscription", SubscriptionController, :create)
+    get("/push/subscription", SubscriptionController, :get)
+    put("/push/subscription", SubscriptionController, :update)
+    delete("/push/subscription", SubscriptionController, :delete)
   end
 
   scope "/api/web", Pleroma.Web.MastodonAPI do
-    pipe_through([:authenticated_api, :oauth_write])
+    pipe_through(:authenticated_api)
 
     put("/settings", MastodonAPIController, :put_settings)
   end
@@ -438,32 +387,28 @@ defmodule Pleroma.Web.Router do
       :account_confirmation_resend
     )
 
-    scope [] do
-      pipe_through(:oauth_read_or_public)
+    get("/timelines/public", MastodonAPIController, :public_timeline)
+    get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
+    get("/timelines/list/:list_id", MastodonAPIController, :list_timeline)
 
-      get("/timelines/public", MastodonAPIController, :public_timeline)
-      get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
-      get("/timelines/list/:list_id", MastodonAPIController, :list_timeline)
+    get("/statuses", MastodonAPIController, :get_statuses)
+    get("/statuses/:id", MastodonAPIController, :get_status)
+    get("/statuses/:id/context", MastodonAPIController, :get_context)
 
-      get("/statuses", MastodonAPIController, :get_statuses)
-      get("/statuses/:id", MastodonAPIController, :get_status)
-      get("/statuses/:id/context", MastodonAPIController, :get_context)
+    get("/polls/:id", MastodonAPIController, :get_poll)
 
-      get("/polls/:id", MastodonAPIController, :get_poll)
+    get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)
+    get("/accounts/:id/followers", MastodonAPIController, :followers)
+    get("/accounts/:id/following", MastodonAPIController, :following)
+    get("/accounts/:id", MastodonAPIController, :user)
 
-      get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)
-      get("/accounts/:id/followers", MastodonAPIController, :followers)
-      get("/accounts/:id/following", MastodonAPIController, :following)
-      get("/accounts/:id", MastodonAPIController, :user)
+    get("/search", SearchController, :search)
 
-      get("/search", SearchController, :search)
-
-      get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)
-    end
+    get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)
   end
 
   scope "/api/v2", Pleroma.Web.MastodonAPI do
-    pipe_through([:api, :oauth_read_or_public])
+    pipe_through(:api)
     get("/search", SearchController, :search2)
   end
 
@@ -494,11 +439,7 @@ defmodule Pleroma.Web.Router do
     get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)
     delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token)
 
-    scope [] do
-      pipe_through(:oauth_read)
-
-      post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
-    end
+    post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
   end
 
   pipeline :ap_service_actor do
@@ -563,26 +504,16 @@ defmodule Pleroma.Web.Router do
   scope "/", Pleroma.Web.ActivityPub do
     pipe_through([:activitypub_client])
 
-    scope [] do
-      pipe_through(:oauth_read)
-      get("/api/ap/whoami", ActivityPubController, :whoami)
-      get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
-    end
-
-    scope [] do
-      pipe_through(:oauth_write)
-      post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
-    end
-
-    scope [] do
-      pipe_through(:oauth_read_or_public)
-      get("/users/:nickname/followers", ActivityPubController, :followers)
-      get("/users/:nickname/following", ActivityPubController, :following)
-    end
+    get("/api/ap/whoami", ActivityPubController, :whoami)
+    get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
+    post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
+    get("/users/:nickname/followers", ActivityPubController, :followers)
+    get("/users/:nickname/following", ActivityPubController, :following)
   end
 
   scope "/", Pleroma.Web.ActivityPub do
     pipe_through(:activitypub)
+
     post("/inbox", ActivityPubController, :inbox)
     post("/users/:nickname/inbox", ActivityPubController, :inbox)
   end
@@ -628,10 +559,7 @@ defmodule Pleroma.Web.Router do
 
     post("/auth/password", MastodonAPIController, :password_reset)
 
-    scope [] do
-      pipe_through(:oauth_read)
-      get("/web/*path", MastodonAPIController, :index)
-    end
+    get("/web/*path", MastodonAPIController, :index)
   end
 
   pipeline :remote_media do
index d7745ae7a979f58cf9c6992e75b6f4ecf33a982e..54f0280c9b096c2e25b2e795327230f29d1fb9b0 100644 (file)
@@ -13,11 +13,32 @@ 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(
+    OAuthScopesPlug,
+    %{scopes: ["write:accounts"]}
+    when action in [
+           :change_email,
+           :change_password,
+           :delete_account,
+           :update_notificaton_settings,
+           :disable_account
+         ]
+  )
+
   plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version])
 
   def help_test(conn, _params) do
index 42234ae09e020019e5d06709d620a986348d4fe6..42bd74eb5c6d22d71de9644c90b78a9d6cbbb228 100644 (file)
@@ -7,12 +7,15 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
 
   alias Ecto.Changeset
   alias Pleroma.Notification
+  alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.User
   alias Pleroma.Web.OAuth.Token
   alias Pleroma.Web.TwitterAPI.TokenView
 
   require Logger
 
+  plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read)
+
   action_fallback(:errors)
 
   def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
index f328026dfac5cc90298e46c41851db0bc923670d..c69e2de4f4cd107a5a44c4b922b903bb23e6ba3c 100644 (file)
@@ -5,24 +5,48 @@
 defmodule Pleroma.Plugs.OAuthScopesPlugTest do
   use Pleroma.Web.ConnCase, async: true
 
+  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
   alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.Repo
 
+  import Mock
   import Pleroma.Factory
 
-  test "proceeds with no op if `assigns[:token]` is nil", %{conn: conn} do
-    conn =
-      conn
-      |> assign(:user, insert(:user))
-      |> OAuthScopesPlug.call(%{scopes: ["read"]})
+  setup_with_mocks([{EnsurePublicOrAuthenticatedPlug, [], [call: fn conn, _ -> conn end]}]) do
+    :ok
+  end
 
-    refute conn.halted
-    assert conn.assigns[:user]
+  describe "when `assigns[:token]` is nil, " do
+    test "with :skip_instance_privacy_check option, proceeds with no op", %{conn: conn} do
+      conn =
+        conn
+        |> assign(:user, insert(:user))
+        |> OAuthScopesPlug.call(%{scopes: ["read"], skip_instance_privacy_check: true})
+
+      refute conn.halted
+      assert conn.assigns[:user]
+
+      refute called(EnsurePublicOrAuthenticatedPlug.call(conn, :_))
+    end
+
+    test "without :skip_instance_privacy_check option, calls EnsurePublicOrAuthenticatedPlug", %{
+      conn: conn
+    } do
+      conn =
+        conn
+        |> assign(:user, insert(:user))
+        |> OAuthScopesPlug.call(%{scopes: ["read"]})
+
+      refute conn.halted
+      assert conn.assigns[:user]
+
+      assert called(EnsurePublicOrAuthenticatedPlug.call(conn, :_))
+    end
   end
 
-  test "proceeds with no op if `token.scopes` fulfill specified 'any of' conditions", %{
-    conn: conn
-  } do
+  test "if `token.scopes` fulfills specified 'any of' conditions, " <>
+         "proceeds with no op",
+       %{conn: conn} do
     token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
 
     conn =
@@ -35,9 +59,9 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
     assert conn.assigns[:user]
   end
 
-  test "proceeds with no op if `token.scopes` fulfill specified 'all of' conditions", %{
-    conn: conn
-  } do
+  test "if `token.scopes` fulfills specified 'all of' conditions, " <>
+         "proceeds with no op",
+       %{conn: conn} do
     token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user)
 
     conn =
@@ -50,73 +74,154 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
     assert conn.assigns[:user]
   end
 
-  test "proceeds with cleared `assigns[:user]` if `token.scopes` doesn't fulfill specified 'any of' conditions " <>
-         "and `fallback: :proceed_unauthenticated` option is specified",
-       %{conn: conn} do
-    token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
+  describe "with `fallback: :proceed_unauthenticated` option, " do
+    test "if `token.scopes` doesn't fulfill specified 'any of' conditions, " <>
+           "clears `assigns[:user]` and calls EnsurePublicOrAuthenticatedPlug",
+         %{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: ["follow"], fallback: :proceed_unauthenticated})
+
+      refute conn.halted
+      refute conn.assigns[:user]
+
+      assert called(EnsurePublicOrAuthenticatedPlug.call(conn, :_))
+    end
+
+    test "if `token.scopes` doesn't fulfill specified 'all of' conditions, " <>
+           "clears `assigns[:user] and calls EnsurePublicOrAuthenticatedPlug",
+         %{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", "follow"],
+          op: :&,
+          fallback: :proceed_unauthenticated
+        })
+
+      refute conn.halted
+      refute conn.assigns[:user]
+
+      assert called(EnsurePublicOrAuthenticatedPlug.call(conn, :_))
+    end
+
+    test "with :skip_instance_privacy_check option, " <>
+           "if `token.scopes` doesn't fulfill specified conditions, " <>
+           "clears `assigns[:user]` and does not call EnsurePublicOrAuthenticatedPlug",
+         %{conn: conn} do
+      token = insert(:oauth_token, scopes: ["read:statuses", "write"]) |> Repo.preload(:user)
+
+      conn =
+        conn
+        |> assign(:user, token.user)
+        |> assign(:token, token)
+        |> OAuthScopesPlug.call(%{
+          scopes: ["read"],
+          fallback: :proceed_unauthenticated,
+          skip_instance_privacy_check: true
+        })
+
+      refute conn.halted
+      refute conn.assigns[:user]
+
+      refute called(EnsurePublicOrAuthenticatedPlug.call(conn, :_))
+    end
+  end
 
-    conn =
-      conn
-      |> assign(:user, token.user)
-      |> assign(:token, token)
-      |> OAuthScopesPlug.call(%{scopes: ["follow"], fallback: :proceed_unauthenticated})
+  describe "without :fallback option, " do
+    test "if `token.scopes` does not fulfill specified 'any of' conditions, " <>
+           "returns 403 and halts",
+         %{conn: conn} do
+      token = insert(:oauth_token, scopes: ["read", "write"])
+      any_of_scopes = ["follow"]
 
-    refute conn.halted
-    refute conn.assigns[:user]
-  end
+      conn =
+        conn
+        |> assign(:token, token)
+        |> OAuthScopesPlug.call(%{scopes: any_of_scopes})
 
-  test "proceeds with cleared `assigns[:user]` if `token.scopes` doesn't fulfill specified 'all of' conditions " <>
-         "and `fallback: :proceed_unauthenticated` option is specified",
-       %{conn: conn} do
-    token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
+      assert conn.halted
+      assert 403 == conn.status
 
-    conn =
-      conn
-      |> assign(:user, token.user)
-      |> assign(:token, token)
-      |> OAuthScopesPlug.call(%{
-        scopes: ["read", "follow"],
-        op: :&,
-        fallback: :proceed_unauthenticated
-      })
+      expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, ", ")}."
+      assert Jason.encode!(%{error: expected_error}) == conn.resp_body
+    end
 
-    refute conn.halted
-    refute conn.assigns[:user]
-  end
+    test "if `token.scopes` does not fulfill specified 'all of' conditions, " <>
+           "returns 403 and halts",
+         %{conn: conn} do
+      token = insert(:oauth_token, scopes: ["read", "write"])
+      all_of_scopes = ["write", "follow"]
 
-  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"]
+      conn =
+        conn
+        |> assign(:token, token)
+        |> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&})
 
-    conn =
-      conn
-      |> assign(:token, token)
-      |> OAuthScopesPlug.call(%{scopes: any_of_scopes})
+      assert conn.halted
+      assert 403 == conn.status
 
-    assert conn.halted
-    assert 403 == conn.status
+      expected_error =
+        "Insufficient permissions: #{Enum.join(all_of_scopes -- token.scopes, ", ")}."
 
-    expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, ", ")}."
-    assert Jason.encode!(%{error: expected_error}) == conn.resp_body
+      assert Jason.encode!(%{error: expected_error}) == conn.resp_body
+    end
   end
 
-  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"]
+  describe "with hierarchical scopes, " do
+    test "if `token.scopes` fulfills specified 'any of' conditions, " <>
+           "proceeds with no op",
+         %{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 "if `token.scopes` fulfills specified 'all of' conditions, " <>
+           "proceeds with no op",
+         %{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
 
-    conn =
-      conn
-      |> assign(:token, token)
-      |> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&})
+  describe "filter_descendants/2" do
+    test "filters scopes which directly match or are ancestors of supported scopes" do
+      f = fn scopes, supported_scopes ->
+        OAuthScopesPlug.filter_descendants(scopes, supported_scopes)
+      end
+
+      assert f.(["read", "follow"], ["write", "read"]) == ["read"]
 
-    assert conn.halted
-    assert 403 == conn.status
+      assert f.(["read", "write:something", "follow"], ["write", "read"]) ==
+               ["read", "write:something"]
 
-    expected_error =
-      "Insufficient permissions: #{Enum.join(all_of_scopes -- token.scopes, ", ")}."
+      assert f.(["admin:read"], ["write", "read"]) == []
 
-    assert Jason.encode!(%{error: expected_error}) == conn.resp_body
+      assert f.(["admin:read"], ["write", "admin"]) == ["admin:read"]
+    end
   end
 end
index 7191150031c703e6565b2746310fd3fe0770522e..c14c8ddb34c756bfa636a8fe00747809da6616a6 100644 (file)
@@ -283,6 +283,7 @@ defmodule Pleroma.Factory do
 
     %Pleroma.Web.OAuth.Token{
       token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
+      scopes: ["read"],
       refresh_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
       user: build(:user),
       app_id: oauth_app.id,
index 87ee82050cf47c65296354cbe4d903558a8d8466..1680ec12277a156a055a0951bfc47bc17d6f427e 100644 (file)
@@ -257,7 +257,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       assert user_response["pleroma"]["background_image"]
     end
 
-    test "requires 'write' permission", %{conn: conn} do
+    test "requires 'write:accounts' permission", %{conn: conn} do
       token1 = insert(:oauth_token, scopes: ["read"])
       token2 = insert(:oauth_token, scopes: ["write", "follow"])
 
@@ -268,7 +268,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
           |> patch("/api/v1/accounts/update_credentials", %{})
 
         if token == token1 do
-          assert %{"error" => "Insufficient permissions: write."} == json_response(conn, 403)
+          assert %{"error" => "Insufficient permissions: write:accounts."} ==
+                   json_response(conn, 403)
         else
           assert json_response(conn, 200)
         end
index b492c77942067d8d9db17811d10da2606ca0fd5f..e919ea112a8e007254b81c16215eaf5933101413 100644 (file)
@@ -556,7 +556,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
             "password" => "test",
             "client_id" => app.client_id,
             "redirect_uri" => redirect_uri,
-            "scope" => "read write",
+            "scope" => "read:subscope write",
             "state" => "statepassed"
           }
         })
@@ -569,7 +569,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
       assert %{"state" => "statepassed", "code" => code} = query
       auth = Repo.get_by(Authorization, token: code)
       assert auth
-      assert auth.scopes == ["read", "write"]
+      assert auth.scopes == ["read:subscope", "write"]
     end
 
     test "returns 401 for wrong credentials", %{conn: conn} do
@@ -626,7 +626,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
       assert result =~ "This action is outside the authorized scopes"
     end
 
-    test "returns 401 for scopes beyond app scopes", %{conn: conn} do
+    test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do
       user = insert(:user)
       app = insert(:oauth_app, scopes: ["read", "write"])
       redirect_uri = OAuthController.default_redirect_uri(app)
index 0a2a48fb703df856c7a9560bbb5a146284a2b53f..65c9f89e8369a45dd168e6cb9038d35e0c506b47 100644 (file)
@@ -81,19 +81,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