From b63faf9819c2c49d2e9b63e7f37136eb03d8b4e8 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sun, 8 Sep 2019 15:00:03 +0300 Subject: [PATCH 01/15] [#1234] Mastodon 2.4.3 hierarchical scopes initial support (WIP). --- lib/pleroma/plugs/oauth_scopes_plug.ex | 20 ++++++++-- .../controllers/mastodon_api_controller.ex | 36 ++++++++++++++++++ lib/pleroma/web/oauth/oauth_controller.ex | 2 +- lib/pleroma/web/oauth/scopes.ex | 14 +++---- lib/pleroma/web/router.ex | 14 +------ .../controllers/util_controller.ex | 9 +++++ test/plugs/oauth_scopes_plug_test.exs | 38 ++++++++++++++++++- test/web/twitter_api/util_controller_test.exs | 10 +++-- 8 files changed, 113 insertions(+), 30 deletions(-) diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex index b508628a9..41403047e 100644 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ b/lib/pleroma/plugs/oauth_scopes_plug.ex @@ -13,15 +13,16 @@ def init(%{scopes: _} = options), do: options 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 @@ def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) 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 @@ def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) 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 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 8dfad7a54..118446c85 100644 --- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -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 diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 81eae2c8b..130ec7895 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -451,7 +451,7 @@ defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), 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 diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex index ad9dfb260..48bd14407 100644 --- a/lib/pleroma/web/oauth/scopes.ex +++ b/lib/pleroma/web/oauth/scopes.ex @@ -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 @@ def to_string(scopes), do: Enum.join(scopes, " ") @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 diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index cfb973f53..8c93e535e 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -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) diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 3405bd3b7..1c6ad5057 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -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 diff --git a/test/plugs/oauth_scopes_plug_test.exs b/test/plugs/oauth_scopes_plug_test.exs index f328026df..9b0a2e702 100644 --- a/test/plugs/oauth_scopes_plug_test.exs +++ b/test/plugs/oauth_scopes_plug_test.exs @@ -84,7 +84,8 @@ test "proceeds with cleared `assigns[:user]` if `token.scopes` doesn't fulfill s 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 @@ test "returns 403 and halts in case of no :fallback option and `token.scopes` no 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 @@ test "returns 403 and halts in case of no :fallback option and `token.scopes` no 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 diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index cf8e69d2b..685e48270 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -78,19 +78,21 @@ test "it imports new-style mastodon follow lists", %{conn: conn} 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 From e6f43a831bdd2a381ed4de493344886f312f9a38 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sun, 15 Sep 2019 18:22:08 +0300 Subject: [PATCH 02/15] [#1234] Permissions-related fixes / new functionality (Masto 2.4.3 scopes). --- lib/pleroma/plugs/oauth_scopes_plug.ex | 12 +- .../activity_pub/activity_pub_controller.ex | 5 + .../web/admin_api/admin_api_controller.ex | 51 +++ .../controllers/list_controller.ex | 9 + .../controllers/mastodon_api_controller.ex | 135 +++++++- .../controllers/search_controller.ex | 5 + .../controllers/subscription_controller.ex | 2 + .../web/pleroma_api/pleroma_api_controller.ex | 13 + lib/pleroma/web/router.ex | 313 +++++++----------- .../controllers/util_controller.ex | 11 + .../web/twitter_api/twitter_api_controller.ex | 3 + test/support/factory.ex | 1 + .../update_credentials_test.exs | 5 +- test/web/oauth/oauth_controller_test.exs | 6 +- 14 files changed, 374 insertions(+), 197 deletions(-) diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex index 41403047e..e0d61c4eb 100644 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ b/lib/pleroma/plugs/oauth_scopes_plug.ex @@ -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 @@ -17,7 +19,7 @@ def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do cond do is_nil(token) -> - conn + maybe_perform_instance_privacy_check(conn, options) op == :| && Enum.any?(matched_scopes) -> conn @@ -29,6 +31,7 @@ def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do conn |> assign(:user, nil) |> assign(:token, nil) + |> maybe_perform_instance_privacy_check(options) true -> missing_scopes = scopes -- matched_scopes @@ -56,4 +59,11 @@ def filter_descendants(scopes, supported_scopes) do 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 diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 08bf1c752..7047b8254 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -23,6 +23,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do action_fallback(:errors) + 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]) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 544b9d7d8..0a508d40e 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -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,56 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do require Logger + 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 [ + :list_reports, + :report_show, + :right_get, + :get_invite_token, + :invites, + :get_password_reset, + :list_users, + :user_show, + :config_show, + :migrate_to_db, + :migrate_from_db, + :list_log + ] + ) + + plug( + OAuthScopesPlug, + %{scopes: ["write"]} + when action in [ + :report_update_state, + :report_respond, + :user_follow, + :user_unfollow, + :user_delete, + :users_create, + :user_toggle_activation, + :tag_users, + :untag_users, + :right_add, + :right_delete, + :set_activation_status, + :relay_follow, + :relay_unfollow, + :revoke_invite, + :email_invite, + :config_update + ] + ) + @users_page_size 50 action_fallback(:errors) diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex index 2873deda8..be7089630 100644 --- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex @@ -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 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 118446c85..704664f5f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -53,6 +53,123 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do require Logger require Pleroma.Constants + plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index) + + @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []} + + plug( + OAuthScopesPlug, + %{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_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] @@ -64,6 +181,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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( @@ -84,8 +202,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do plug( OAuthScopesPlug, - %{scopes: ["write:mutes"]} - when action in [:mute_conversation, :unmute_conversation] + %{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] ) @rate_limited_relations_actions ~w(follow unfollow)a @@ -776,7 +901,7 @@ def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _para 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 @@ -1488,6 +1613,8 @@ def empty_object(conn, _) 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) @@ -1610,7 +1737,7 @@ def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) 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 diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 9072aa7a4..f49ca89ed 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -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 diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex index e2b17aab1..287eebf92 100644 --- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex @@ -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 # diff --git a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex index f4df3b024..17c568a9d 100644 --- a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex @@ -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 diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 8c93e535e..593da01fd 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -87,27 +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_push do - plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]}) - end - pipeline :well_known do plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"]) end @@ -149,7 +128,12 @@ defmodule Pleroma.Web.Router do end scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do - pipe_through([:admin_api, :oauth_write]) + pipe_through(:admin_api) + + get("/reports", AdminAPIController, :list_reports) + get("/reports/:id", AdminAPIController, :report_show) + put("/reports/:id", AdminAPIController, :report_update_state) + post("/reports/:id/respond", AdminAPIController, :report_respond) post("/users/follow", AdminAPIController, :user_follow) post("/users/unfollow", AdminAPIController, :user_unfollow) @@ -184,15 +168,6 @@ defmodule Pleroma.Web.Router do get("/users", AdminAPIController, :list_users) get("/users/:nickname", AdminAPIController, :user_show) - get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses) - - get("/reports", AdminAPIController, :list_reports) - get("/reports/:id", AdminAPIController, :report_show) - put("/reports/:id", AdminAPIController, :report_update_state) - post("/reports/:id/respond", AdminAPIController, :report_respond) - - put("/statuses/:id", AdminAPIController, :status_update) - delete("/statuses/:id", AdminAPIController, :status_delete) get("/config", AdminAPIController, :config_show) post("/config", AdminAPIController, :config_update) @@ -200,6 +175,10 @@ defmodule Pleroma.Web.Router do get("/config/migrate_from_db", AdminAPIController, :migrate_from_db) get("/moderation_log", AdminAPIController, :list_log) + + get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses) + put("/statuses/:id", AdminAPIController, :status_update) + delete("/statuses/:id", AdminAPIController, :status_delete) end scope "/", Pleroma.Web.TwitterAPI do @@ -213,19 +192,13 @@ defmodule Pleroma.Web.Router do scope "/api/pleroma", Pleroma.Web.TwitterAPI do pipe_through(:authenticated_api) - scope [] do - pipe_through(:oauth_write) + 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) - 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 - - scope [] do - 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 @@ -252,148 +225,134 @@ 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 - - scope [] do - pipe_through(:oauth_write) - patch("/conversations/:id", PleromaAPIController, :update_conversation) - post("/notifications/read", PleromaAPIController, :read_notification) - end + get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses) + get("/conversations/:id", PleromaAPIController, :conversation) + 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("/blocks", MastodonAPIController, :blocks) + get("/mutes", MastodonAPIController, :mutes) + get("/domain_blocks", MastodonAPIController, :domain_blocks) - get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials) + get("/accounts/:id/lists", MastodonAPIController, :account_lists) + get("/lists", ListController, :index) + get("/lists/:id", ListController, :show) + get("/lists/:id/accounts", ListController, :list_accounts) - get("/accounts/relationships", MastodonAPIController, :relationships) + post("/notifications/clear", MastodonAPIController, :clear_notifications) + post("/notifications/dismiss", MastodonAPIController, :dismiss_notification) + get("/notifications", MastodonAPIController, :notifications) + get("/notifications/:id", MastodonAPIController, :get_notification) - get("/accounts/:id/lists", MastodonAPIController, :account_lists) - get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array) + delete( + "/notifications/destroy_multiple", + MastodonAPIController, + :destroy_multiple_notifications + ) - get("/follow_requests", MastodonAPIController, :follow_requests) - get("/blocks", MastodonAPIController, :blocks) - get("/mutes", MastodonAPIController, :mutes) + # Note: not present in Mastodon + get("/bookmarks", MastodonAPIController, :bookmarks) - get("/timelines/home", MastodonAPIController, :home_timeline) - get("/timelines/direct", MastodonAPIController, :dm_timeline) + get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array) - get("/favourites", MastodonAPIController, :favourites) - get("/bookmarks", MastodonAPIController, :bookmarks) + get("/favourites", MastodonAPIController, :favourites) - 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("/accounts/relationships", MastodonAPIController, :relationships) - get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses) - get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status) + get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials) - get("/lists", ListController, :index) - get("/lists/:id", ListController, :show) - get("/lists/:id/accounts", ListController, :list_accounts) + get("/timelines/home", MastodonAPIController, :home_timeline) + get("/timelines/direct", MastodonAPIController, :dm_timeline) - get("/domain_blocks", MastodonAPIController, :domain_blocks) + get("/suggestions", MastodonAPIController, :suggestions) + get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses) + get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status) + get("/follow_requests", MastodonAPIController, :follow_requests) + get("/filters", MastodonAPIController, :get_filters) + get("/endorsements", MastodonAPIController, :endorsements) + get("/conversations", MastodonAPIController, :conversations) + post("/conversations/:id/read", MastodonAPIController, :conversation_read) - get("/filters", MastodonAPIController, :get_filters) + delete("/lists/:id", ListController, :delete) + post("/lists", ListController, :create) + put("/lists/:id", ListController, :update) - get("/suggestions", MastodonAPIController, :suggestions) + post("/lists/:id/accounts", ListController, :add_to_list) + delete("/lists/:id/accounts", ListController, :remove_from_list) - get("/conversations", MastodonAPIController, :conversations) - post("/conversations/:id/read", MastodonAPIController, :conversation_read) + post("/reports", MastodonAPIController, :create_report) - get("/endorsements", MastodonAPIController, :empty_array) - 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_write) + get("/pleroma/mascot", MastodonAPIController, :get_mascot) + put("/pleroma/mascot", MastodonAPIController, :set_mascot) - patch("/accounts/update_credentials", MastodonAPIController, :update_credentials) + post("/media", MastodonAPIController, :upload) + put("/media/:id", MastodonAPIController, :update_media) - post("/statuses", MastodonAPIController, :post_status) - delete("/statuses/:id", MastodonAPIController, :delete_status) + patch("/accounts/update_credentials", MastodonAPIController, :update_credentials) - 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) + post("/polls/:id/votes", MastodonAPIController, :poll_vote) - put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status) - delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status) + post("/statuses/:id/reblog", MastodonAPIController, :reblog_status) + post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status) - post("/polls/:id/votes", MastodonAPIController, :poll_vote) + post("/statuses/:id/pin", MastodonAPIController, :pin_status) + post("/statuses/:id/unpin", MastodonAPIController, :unpin_status) - post("/media", MastodonAPIController, :upload) - put("/media/:id", MastodonAPIController, :update_media) + post("/statuses/:id/mute", MastodonAPIController, :mute_conversation) + post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation) - delete("/lists/:id", ListController, :delete) - post("/lists", ListController, :create) - put("/lists/:id", ListController, :update) + post("/statuses/:id/favourite", MastodonAPIController, :fav_status) + post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status) - post("/lists/:id/accounts", ListController, :add_to_list) - delete("/lists/:id/accounts", ListController, :remove_from_list) + post("/statuses", MastodonAPIController, :post_status) + delete("/statuses/:id", MastodonAPIController, :delete_status) - post("/filters", MastodonAPIController, :create_filter) - get("/filters/:id", MastodonAPIController, :get_filter) - put("/filters/:id", MastodonAPIController, :update_filter) - delete("/filters/:id", MastodonAPIController, :delete_filter) + put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status) + delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status) - 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("/filters", MastodonAPIController, :create_filter) + get("/filters/:id", MastodonAPIController, :get_filter) + put("/filters/:id", MastodonAPIController, :update_filter) + delete("/filters/:id", MastodonAPIController, :delete_filter) - 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, :reports) - end + 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) - scope [] do - 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) - post("/domain_blocks", MastodonAPIController, :block_domain) - delete("/domain_blocks", MastodonAPIController, :unblock_domain) + post("/push/subscription", SubscriptionController, :create) + get("/push/subscription", SubscriptionController, :get) + put("/push/subscription", SubscriptionController, :update) + delete("/push/subscription", SubscriptionController, :delete) - post("/pleroma/accounts/:id/subscribe", MastodonAPIController, :subscribe) - post("/pleroma/accounts/:id/unsubscribe", MastodonAPIController, :unsubscribe) - end - - scope [] do - pipe_through(:oauth_push) - - post("/push/subscription", SubscriptionController, :create) - get("/push/subscription", SubscriptionController, :get) - put("/push/subscription", SubscriptionController, :update) - delete("/push/subscription", SubscriptionController, :delete) - end + # Note: not present in Mastodon: bookmark, unbookmark + post("/statuses/:id/bookmark", MastodonAPIController, :bookmark_status) + post("/statuses/:id/unbookmark", MastodonAPIController, :unbookmark_status) end scope "/api/web", Pleroma.Web.MastodonAPI do - pipe_through([:authenticated_api, :oauth_write]) + pipe_through(:authenticated_api) put("/settings", MastodonAPIController, :put_settings) end @@ -424,31 +383,29 @@ 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/public", MastodonAPIController, :public_timeline) - get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline) - get("/timelines/list/:list_id", MastodonAPIController, :list_timeline) + get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites) - get("/statuses/:id", MastodonAPIController, :get_status) - get("/statuses/:id/context", MastodonAPIController, :get_context) + get("/search", SearchController, :search) - 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/followers", MastodonAPIController, :followers) + get("/accounts/:id/following", MastodonAPIController, :following) - get("/search", SearchController, :search) + get("/timelines/list/:list_id", MastodonAPIController, :list_timeline) - get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites) - end + get("/accounts/:id", MastodonAPIController, :user) + + get("/accounts/:id/statuses", MastodonAPIController, :user_statuses) + get("/statuses/:id", MastodonAPIController, :get_status) + get("/statuses/:id/context", MastodonAPIController, :get_context) end scope "/api/v2", Pleroma.Web.MastodonAPI do - pipe_through([:api, :oauth_read_or_public]) + pipe_through(:api) get("/search", SearchController, :search2) end @@ -478,12 +435,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 @@ -547,26 +499,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 @@ -612,10 +554,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 diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 1c6ad5057..82ed0c287 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -27,6 +27,17 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks_import) + plug( + OAuthScopesPlug, + %{scopes: ["write:accounts"]} + when action in [ + :change_password, + :delete_account, + :update_notificaton_settings, + :disable_account + ] + ) + plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version]) def help_test(conn, _params) do diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 42234ae09..42bd74eb5 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -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 diff --git a/test/support/factory.ex b/test/support/factory.ex index 719115003..c14c8ddb3 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -283,6 +283,7 @@ def oauth_token_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, diff --git a/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs index 87ee82050..1680ec122 100644 --- a/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs +++ b/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs @@ -257,7 +257,7 @@ test "updates the user's background", %{conn: conn} 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 @@ test "requires 'write' permission", %{conn: conn} 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 diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index b492c7794..e919ea112 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -556,7 +556,7 @@ test "redirects with oauth authorization" 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 @@ test "redirects with oauth authorization" 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 @@ test "returns 401 for missing scopes", %{conn: conn} 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) From e7afb67c5c26fcb07c1f8d4390c0573cebef9520 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Tue, 17 Sep 2019 16:16:11 +0300 Subject: [PATCH 03/15] [#1260] Rate-limiting for create authentication and related requests. --- config/config.exs | 2 +- config/description.exs | 9 ++++++++- lib/pleroma/web/mongooseim/mongoose_im_controller.ex | 5 +++++ lib/pleroma/web/oauth/oauth_controller.ex | 1 + 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/config/config.exs b/config/config.exs index c7e0cf09f..15979702f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -585,7 +585,7 @@ config :http_signatures, adapter: Pleroma.Signature -config :pleroma, :rate_limit, nil +config :pleroma, :rate_limit, authentication: {60_000, 15} config :pleroma, Pleroma.ActivityExpiration, enabled: true diff --git a/config/description.exs b/config/description.exs index 32d36d6d6..bd0378e00 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2424,7 +2424,8 @@ group: :pleroma, key: :rate_limit, type: :group, - description: "Rate limit settings. This is an advanced feature and disabled by default.", + description: + "Rate limit settings. This is an advanced feature enabled only for :authentication by default.", children: [ %{ key: :search, @@ -2463,6 +2464,12 @@ description: "for fav / unfav or reblog / unreblog actions on the same status by the same user", suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]] + }, + %{ + key: :authentication, + type: [:tuple, {:list, :tuple}], + description: "for authentication create / password check / user existence check requests", + suggestions: [{60_000, 15}] } ] }, diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex index b786a521b..6ed181cff 100644 --- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex +++ b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex @@ -4,10 +4,15 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do use Pleroma.Web, :controller + alias Comeonin.Pbkdf2 + alias Pleroma.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.User + plug(RateLimiter, :authentication when action in [:user_exists, :check_password]) + plug(RateLimiter, {:authentication, params: ["user"]} when action == :check_password) + def user_exists(conn, %{"user" => username}) do with %User{} <- Repo.get_by(User, nickname: username, local: true) do conn diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 81eae2c8b..281c7d2d8 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -24,6 +24,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do plug(:fetch_session) plug(:fetch_flash) + plug(Pleroma.Plugs.RateLimiter, :authentication when action == :create_authorization) action_fallback(Pleroma.Web.OAuth.FallbackController) From 76068873dbf9da191dd2487158ca88df198b811a Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Tue, 17 Sep 2019 22:19:39 +0300 Subject: [PATCH 04/15] [#1234] Defined admin OAuth scopes, refined other scopes. Added tests. --- .../web/admin_api/admin_api_controller.ex | 71 +++--- .../controllers/mastodon_api_controller.ex | 23 +- test/plugs/oauth_scopes_plug_test.exs | 227 ++++++++++++------ 3 files changed, 210 insertions(+), 111 deletions(-) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 0a508d40e..fa69a23d9 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -24,38 +24,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do require Logger - plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :list_user_statuses) - plug( OAuthScopesPlug, - %{scopes: ["write:statuses"]} when action in [:status_update, :status_delete] + %{scopes: ["admin:read:accounts", "read:accounts"]} + when action in [:list_users, :user_show, :right_get, :invites] ) plug( OAuthScopesPlug, - %{scopes: ["read"]} + %{scopes: ["admin:write", "write:accounts"]} when action in [ - :list_reports, - :report_show, - :right_get, :get_invite_token, - :invites, + :revoke_invite, + :email_invite, :get_password_reset, - :list_users, - :user_show, - :config_show, - :migrate_to_db, - :migrate_from_db, - :list_log - ] - ) - - plug( - OAuthScopesPlug, - %{scopes: ["write"]} - when action in [ - :report_update_state, - :report_respond, :user_follow, :user_unfollow, :user_delete, @@ -65,15 +47,44 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do :untag_users, :right_add, :right_delete, - :set_activation_status, - :relay_follow, - :relay_unfollow, - :revoke_invite, - :email_invite, - :config_update + :set_activation_status ] ) + plug( + OAuthScopesPlug, + %{scopes: ["admin:read:reports", "read:reports"]} when action in [:list_reports, :report_show] + ) + + plug( + OAuthScopesPlug, + %{scopes: ["admin:write:reports", "write:reports"]} + when action in [:report_update_state, :report_respond] + ) + + plug( + OAuthScopesPlug, + %{scopes: ["admin:read:statuses", "read:statuses"]} when action == :list_user_statuses + ) + + plug( + OAuthScopesPlug, + %{scopes: ["admin:write:statuses", "write:statuses"]} + when action in [:status_update, :status_delete] + ) + + plug( + OAuthScopesPlug, + %{scopes: ["admin:read", "read"]} + when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log] + ) + + plug( + OAuthScopesPlug, + %{scopes: ["admin:write", "write"]} + when action in [:relay_follow, :relay_unfollow, :config_update] + ) + @users_page_size 50 action_fallback(:errors) @@ -451,7 +462,7 @@ def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) 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) 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 c5632bb5e..d7a83a2f5 100644 --- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -53,13 +53,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do require Logger require Pleroma.Constants - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index) - @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []} + # Note: :index action handles attempt of unauthenticated access to private instance with redirect plug( OAuthScopesPlug, - %{scopes: ["read"], skip_instance_privacy_check: true} when action == :index + Map.merge(@unauthenticated_access, %{scopes: ["read"], skip_instance_privacy_check: true}) + when action == :index ) plug( @@ -220,6 +220,23 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do %{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 diff --git a/test/plugs/oauth_scopes_plug_test.exs b/test/plugs/oauth_scopes_plug_test.exs index 9b0a2e702..3b895a6e4 100644 --- a/test/plugs/oauth_scopes_plug_test.exs +++ b/test/plugs/oauth_scopes_plug_test.exs @@ -6,23 +6,47 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do use Pleroma.Web.ConnCase, async: true alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug 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"]}) - - refute conn.halted - assert conn.assigns[:user] + setup_with_mocks([{EnsurePublicOrAuthenticatedPlug, [], [call: fn conn, _ -> conn end]}]) do + :ok end - test "proceeds with no op if `token.scopes` fulfill specified 'any of' conditions", %{ - conn: conn - } do + 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 "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 @@ test "proceeds with no op if `token.scopes` fulfill specified 'any of' condition 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,82 +74,112 @@ test "proceeds with no op if `token.scopes` fulfill specified 'all of' condition 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}) + conn = + conn + |> assign(:user, token.user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: ["follow"], fallback: :proceed_unauthenticated}) - refute conn.halted - refute conn.assigns[:user] + 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 - 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) + 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"] - conn = - conn - |> assign(:user, token.user) - |> assign(:token, token) - |> OAuthScopesPlug.call(%{ - scopes: ["read", "follow"], - op: :&, - fallback: :proceed_unauthenticated - }) + conn = + conn + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: any_of_scopes}) - refute conn.halted - refute conn.assigns[:user] - end + assert conn.halted + assert 403 == conn.status - 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"] + expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, ", ")}." + assert Jason.encode!(%{error: expected_error}) == conn.resp_body + end - conn = - conn - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: any_of_scopes}) + 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"] - assert conn.halted - assert 403 == conn.status + conn = + conn + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&}) - expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, ", ")}." - assert Jason.encode!(%{error: expected_error}) == conn.resp_body - end + assert conn.halted + assert 403 == conn.status - 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"] + expected_error = + "Insufficient permissions: #{Enum.join(all_of_scopes -- token.scopes, ", ")}." - conn = - conn - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&}) - - assert conn.halted - assert 403 == conn.status - - expected_error = - "Insufficient permissions: #{Enum.join(all_of_scopes -- token.scopes, ", ")}." - - assert Jason.encode!(%{error: expected_error}) == conn.resp_body + assert Jason.encode!(%{error: expected_error}) == conn.resp_body + end end describe "with hierarchical scopes, " do - 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 = @@ -138,9 +192,9 @@ test "proceeds with no op if `token.scopes` fulfill specified 'any of' condition 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 = @@ -153,4 +207,21 @@ test "proceeds with no op if `token.scopes` fulfill specified 'all of' condition assert conn.assigns[:user] end end + + 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 f.(["read", "write:something", "follow"], ["write", "read"]) == + ["read", "write:something"] + + assert f.(["admin:read"], ["write", "read"]) == [] + + assert f.(["admin:read"], ["write", "admin"]) == ["admin:read"] + end + end end From b4d775161cdbd1e72ed9bdf3363090c30e8c25fc Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Tue, 17 Sep 2019 23:24:55 +0300 Subject: [PATCH 05/15] [#1234] Undone reordering-related changes in router.ex. --- lib/pleroma/web/router.ex | 186 +++++++++++++++++++------------------- 1 file changed, 95 insertions(+), 91 deletions(-) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 6e80efec4..8448a00a1 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -131,11 +131,6 @@ defmodule Pleroma.Web.Router do scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do pipe_through(:admin_api) - get("/reports", AdminAPIController, :list_reports) - get("/reports/:id", AdminAPIController, :report_show) - put("/reports/:id", AdminAPIController, :report_update_state) - post("/reports/:id/respond", AdminAPIController, :report_respond) - post("/users/follow", AdminAPIController, :user_follow) post("/users/unfollow", AdminAPIController, :user_unfollow) @@ -169,6 +164,15 @@ defmodule Pleroma.Web.Router do get("/users", AdminAPIController, :list_users) get("/users/:nickname", AdminAPIController, :user_show) + get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses) + + get("/reports", AdminAPIController, :list_reports) + get("/reports/:id", AdminAPIController, :report_show) + put("/reports/:id", AdminAPIController, :report_update_state) + post("/reports/:id/respond", AdminAPIController, :report_respond) + + put("/statuses/:id", AdminAPIController, :status_update) + delete("/statuses/:id", AdminAPIController, :status_delete) get("/config", AdminAPIController, :config_show) post("/config", AdminAPIController, :config_update) @@ -176,10 +180,6 @@ defmodule Pleroma.Web.Router do get("/config/migrate_from_db", AdminAPIController, :migrate_from_db) get("/moderation_log", AdminAPIController, :list_log) - - get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses) - put("/statuses/:id", AdminAPIController, :status_update) - delete("/statuses/:id", AdminAPIController, :status_delete) end scope "/", Pleroma.Web.TwitterAPI do @@ -229,6 +229,7 @@ defmodule Pleroma.Web.Router do get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses) get("/conversations/:id", PleromaAPIController, :conversation) + patch("/conversations/:id", PleromaAPIController, :update_conversation) post("/notifications/read", PleromaAPIController, :read_notification) end @@ -236,14 +237,23 @@ defmodule Pleroma.Web.Router do scope "/api/v1", Pleroma.Web.MastodonAPI do pipe_through(:authenticated_api) - get("/blocks", MastodonAPIController, :blocks) - get("/mutes", MastodonAPIController, :mutes) - get("/domain_blocks", MastodonAPIController, :domain_blocks) + get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials) + + get("/accounts/relationships", MastodonAPIController, :relationships) get("/accounts/:id/lists", MastodonAPIController, :account_lists) - get("/lists", ListController, :index) - get("/lists/:id", ListController, :show) - get("/lists/:id/accounts", ListController, :list_accounts) + get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array) + + get("/follow_requests", MastodonAPIController, :follow_requests) + get("/blocks", MastodonAPIController, :blocks) + get("/mutes", MastodonAPIController, :mutes) + + get("/timelines/home", MastodonAPIController, :home_timeline) + get("/timelines/direct", MastodonAPIController, :dm_timeline) + + get("/favourites", MastodonAPIController, :favourites) + # Note: not present in Mastodon: bookmarks + get("/bookmarks", MastodonAPIController, :bookmarks) post("/notifications/clear", MastodonAPIController, :clear_notifications) post("/notifications/dismiss", MastodonAPIController, :dismiss_notification) @@ -256,29 +266,50 @@ defmodule Pleroma.Web.Router do :destroy_multiple_notifications ) - # Note: not present in Mastodon - get("/bookmarks", MastodonAPIController, :bookmarks) - - get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array) - - get("/favourites", MastodonAPIController, :favourites) - - get("/accounts/relationships", MastodonAPIController, :relationships) - - get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials) - - get("/timelines/home", MastodonAPIController, :home_timeline) - get("/timelines/direct", MastodonAPIController, :dm_timeline) - - get("/suggestions", MastodonAPIController, :suggestions) get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses) get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status) - get("/follow_requests", MastodonAPIController, :follow_requests) + + get("/lists", ListController, :index) + get("/lists/:id", ListController, :show) + get("/lists/:id/accounts", ListController, :list_accounts) + + get("/domain_blocks", MastodonAPIController, :domain_blocks) + get("/filters", MastodonAPIController, :get_filters) - get("/endorsements", MastodonAPIController, :endorsements) + + get("/suggestions", MastodonAPIController, :suggestions) + get("/conversations", MastodonAPIController, :conversations) post("/conversations/:id/read", MastodonAPIController, :conversation_read) + get("/endorsements", MastodonAPIController, :endorsements) + + patch("/accounts/update_credentials", MastodonAPIController, :update_credentials) + + post("/statuses", MastodonAPIController, :post_status) + delete("/statuses/:id", MastodonAPIController, :delete_status) + + 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) + + put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status) + delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status) + + post("/polls/:id/votes", MastodonAPIController, :poll_vote) + + post("/media", MastodonAPIController, :upload) + put("/media/:id", MastodonAPIController, :update_media) + delete("/lists/:id", ListController, :delete) post("/lists", ListController, :create) put("/lists/:id", ListController, :update) @@ -286,7 +317,10 @@ defmodule Pleroma.Web.Router do post("/lists/:id/accounts", ListController, :add_to_list) delete("/lists/:id/accounts", ListController, :remove_from_list) - post("/reports", MastodonAPIController, :create_report) + post("/filters", MastodonAPIController, :create_filter) + get("/filters/:id", MastodonAPIController, :get_filter) + put("/filters/:id", MastodonAPIController, :update_filter) + delete("/filters/:id", MastodonAPIController, :delete_filter) patch("/pleroma/accounts/update_avatar", MastodonAPIController, :update_avatar) patch("/pleroma/accounts/update_banner", MastodonAPIController, :update_banner) @@ -295,35 +329,7 @@ defmodule Pleroma.Web.Router do get("/pleroma/mascot", MastodonAPIController, :get_mascot) put("/pleroma/mascot", MastodonAPIController, :set_mascot) - post("/media", MastodonAPIController, :upload) - put("/media/:id", MastodonAPIController, :update_media) - - patch("/accounts/update_credentials", MastodonAPIController, :update_credentials) - - post("/polls/:id/votes", MastodonAPIController, :poll_vote) - - post("/statuses/:id/reblog", MastodonAPIController, :reblog_status) - post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status) - - post("/statuses/:id/pin", MastodonAPIController, :pin_status) - post("/statuses/:id/unpin", MastodonAPIController, :unpin_status) - - post("/statuses/:id/mute", MastodonAPIController, :mute_conversation) - post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation) - - post("/statuses/:id/favourite", MastodonAPIController, :fav_status) - post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status) - - post("/statuses", MastodonAPIController, :post_status) - delete("/statuses/:id", MastodonAPIController, :delete_status) - - put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status) - delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status) - - 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, :create_report) post("/follows", MastodonAPIController, :follow) post("/accounts/:id/follow", MastodonAPIController, :follow) @@ -347,10 +353,6 @@ defmodule Pleroma.Web.Router do get("/push/subscription", SubscriptionController, :get) put("/push/subscription", SubscriptionController, :update) delete("/push/subscription", SubscriptionController, :delete) - - # Note: not present in Mastodon: bookmark, unbookmark - post("/statuses/:id/bookmark", MastodonAPIController, :bookmark_status) - post("/statuses/:id/unbookmark", MastodonAPIController, :unbookmark_status) end scope "/api/web", Pleroma.Web.MastodonAPI do @@ -362,46 +364,47 @@ defmodule Pleroma.Web.Router do scope "/api/v1", Pleroma.Web.MastodonAPI do pipe_through(:api) + post("/accounts", MastodonAPIController, :account_register) + get("/instance", MastodonAPIController, :masto_instance) get("/instance/peers", MastodonAPIController, :peers) - post("/apps", MastodonAPIController, :create_app) get("/apps/verify_credentials", MastodonAPIController, :verify_app_credentials) - get("/custom_emojis", MastodonAPIController, :custom_emojis) + get("/statuses/:id/card", MastodonAPIController, :status_card) + + get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by) + get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by) + get("/trends", MastodonAPIController, :empty_array) get("/accounts/search", SearchController, :account_search) - get("/timelines/public", MastodonAPIController, :public_timeline) - get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline) - get("/timelines/list/:list_id", MastodonAPIController, :list_timeline) - - get("/polls/:id", MastodonAPIController, :get_poll) - - post("/accounts", MastodonAPIController, :account_register) - get("/accounts/:id", MastodonAPIController, :user) - get("/accounts/:id/followers", MastodonAPIController, :followers) - get("/accounts/:id/following", MastodonAPIController, :following) - get("/accounts/:id/statuses", MastodonAPIController, :user_statuses) - - get("/search", SearchController, :search) - - get("/statuses", MastodonAPIController, :get_statuses) - get("/statuses/:id", MastodonAPIController, :get_status) - get("/statuses/:id/context", MastodonAPIController, :get_context) - get("/statuses/:id/card", MastodonAPIController, :status_card) - get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by) - get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by) - - get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites) - post( "/pleroma/accounts/confirmation_resend", MastodonAPIController, :account_confirmation_resend ) + + 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("/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("/search", SearchController, :search) + + get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites) end scope "/api/v2", Pleroma.Web.MastodonAPI do @@ -435,6 +438,7 @@ 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) end From b17f217bf3a16da0e98e63b59da22f40beec809d Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Tue, 17 Sep 2019 23:31:05 +0300 Subject: [PATCH 06/15] [#1234] Addressed code analysis issue. --- test/plugs/oauth_scopes_plug_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/plugs/oauth_scopes_plug_test.exs b/test/plugs/oauth_scopes_plug_test.exs index 3b895a6e4..c69e2de4f 100644 --- a/test/plugs/oauth_scopes_plug_test.exs +++ b/test/plugs/oauth_scopes_plug_test.exs @@ -5,8 +5,8 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do use Pleroma.Web.ConnCase, async: true - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Repo import Mock From 9f28249dfb0918cfd97450494c6c306dae9ab3fb Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 18 Sep 2019 12:06:12 +0300 Subject: [PATCH 07/15] [#1260] Added changelog entry. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eb72c002..1a86881c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,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`) +- Authentication: Added rate limit for password-authorized actions / login existence checks. ### Changed - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text From 551f2fa59eec11c94eb0ab187c06ccab7b8d5647 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 18 Sep 2019 12:31:33 +0300 Subject: [PATCH 08/15] [#1234] Added changelog entry, removed admin OAuth scopes. --- CHANGELOG.md | 1 + .../web/admin_api/admin_api_controller.ex | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 584386136..7e2c8066f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,6 +117,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 diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 2c9840580..7f1a8e566 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -26,13 +26,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, - %{scopes: ["admin:read:accounts", "read:accounts"]} + %{scopes: ["read:accounts"]} when action in [:list_users, :user_show, :right_get, :invites] ) plug( OAuthScopesPlug, - %{scopes: ["admin:write", "write:accounts"]} + %{scopes: ["write:accounts"]} when action in [ :get_invite_token, :revoke_invite, @@ -53,35 +53,35 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, - %{scopes: ["admin:read:reports", "read:reports"]} when action in [:list_reports, :report_show] + %{scopes: ["read:reports"]} when action in [:list_reports, :report_show] ) plug( OAuthScopesPlug, - %{scopes: ["admin:write:reports", "write:reports"]} + %{scopes: ["write:reports"]} when action in [:report_update_state, :report_respond] ) plug( OAuthScopesPlug, - %{scopes: ["admin:read:statuses", "read:statuses"]} when action == :list_user_statuses + %{scopes: ["read:statuses"]} when action == :list_user_statuses ) plug( OAuthScopesPlug, - %{scopes: ["admin:write:statuses", "write:statuses"]} + %{scopes: ["write:statuses"]} when action in [:status_update, :status_delete] ) plug( OAuthScopesPlug, - %{scopes: ["admin:read", "read"]} + %{scopes: ["read"]} when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log] ) plug( OAuthScopesPlug, - %{scopes: ["admin:write", "write"]} + %{scopes: ["write"]} when action in [:relay_follow, :relay_unfollow, :config_update] ) From e4f3d7f69d5d3db787f8b4906cacc673e2f970f1 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 18 Sep 2019 10:31:10 +0000 Subject: [PATCH 09/15] Apply suggestion to lib/pleroma/plugs/oauth_scopes_plug.ex --- lib/pleroma/plugs/oauth_scopes_plug.ex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex index e0d61c4eb..a3278dbef 100644 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ b/lib/pleroma/plugs/oauth_scopes_plug.ex @@ -61,9 +61,10 @@ def filter_descendants(scopes, supported_scopes) do end defp maybe_perform_instance_privacy_check(%Plug.Conn{} = conn, options) do - case options[:skip_instance_privacy_check] do - true -> conn - _ -> EnsurePublicOrAuthenticatedPlug.call(conn, []) + if options[:skip_instance_privacy_check] do + conn + else + EnsurePublicOrAuthenticatedPlug.call(conn, []) end end end From fefbd31c6e60e25778ed01c522273375d4f04266 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 6 Oct 2019 16:22:35 +0300 Subject: [PATCH 10/15] Move local keys out of `user.info` --- lib/pleroma/signature.ex | 2 +- lib/pleroma/user.ex | 8 ++++++-- lib/pleroma/web/activity_pub/views/user_view.ex | 4 ++-- lib/pleroma/web/salmon/salmon.ex | 4 ++-- priv/repo/migrations/20191006123824_add_keys_column.exs | 9 +++++++++ .../20191006135457_move_keys_to_separate_column.exs | 7 +++++++ test/signature_test.exs | 5 ++--- test/user_test.exs | 8 ++++---- 8 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 priv/repo/migrations/20191006123824_add_keys_column.exs create mode 100644 priv/repo/migrations/20191006135457_move_keys_to_separate_column.exs diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index f20aeb0d5..1e7c9ae86 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -48,7 +48,7 @@ def refetch_public_key(conn) do end def sign(%User{} = user, headers) do - with {:ok, %{info: %{keys: keys}}} <- User.ensure_keys_present(user), + with {:ok, %{keys: keys}} <- User.ensure_keys_present(user), {:ok, private_key, _} <- Keys.keys_from_pem(keys) do HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers) end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 0d665afa6..2cfb13a8c 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -51,6 +51,7 @@ defmodule Pleroma.User do field(:password_hash, :string) field(:password, :string, virtual: true) field(:password_confirmation, :string, virtual: true) + field(:keys, :string) field(:following, {:array, :string}, default: []) field(:ap_id, :string) field(:avatar, :map) @@ -1554,11 +1555,14 @@ def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do } end - def ensure_keys_present(%{info: %{keys: keys}} = user) when not is_nil(keys), do: {:ok, user} + def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user} def ensure_keys_present(%User{} = user) do with {:ok, pem} <- Keys.generate_rsa_pem() do - update_info(user, &User.Info.set_keys(&1, pem)) + user + |> cast(%{keys: pem}, [:keys]) + |> validate_required([:keys]) + |> update_and_set_cache() end end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 6bc55c85b..9b39d1629 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -33,7 +33,7 @@ def render("endpoints.json", _), do: %{} def render("service.json", %{user: user}) do {:ok, user} = User.ensure_keys_present(user) - {:ok, _, public_key} = Keys.keys_from_pem(user.info.keys) + {:ok, _, public_key} = Keys.keys_from_pem(user.keys) public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) public_key = :public_key.pem_encode([public_key]) @@ -69,7 +69,7 @@ def render("user.json", %{user: %User{nickname: "internal." <> _} = user}), def render("user.json", %{user: user}) do {:ok, user} = User.ensure_keys_present(user) - {:ok, _, public_key} = Keys.keys_from_pem(user.info.keys) + {:ok, _, public_key} = Keys.keys_from_pem(user.keys) public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) public_key = :public_key.pem_encode([public_key]) diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index 8ba7380c0..0ffe903cd 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -202,7 +202,7 @@ def is_representable?(_), do: false @spec publish(User.t(), Pleroma.Activity.t()) :: none def publish(user, activity) - def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity) + def publish(%{keys: keys} = user, %{data: %{"type" => type}} = activity) when type in @supported_activities do feed = ActivityRepresenter.to_simple_form(activity, user, true) @@ -238,7 +238,7 @@ def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end) def gather_webfinger_links(%User{} = user) do - {:ok, _private, public} = Keys.keys_from_pem(user.info.keys) + {:ok, _private, public} = Keys.keys_from_pem(user.keys) magic_key = encode_key(public) [ diff --git a/priv/repo/migrations/20191006123824_add_keys_column.exs b/priv/repo/migrations/20191006123824_add_keys_column.exs new file mode 100644 index 000000000..b6c615646 --- /dev/null +++ b/priv/repo/migrations/20191006123824_add_keys_column.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.AddKeysColumn do + use Ecto.Migration + + def change do + alter table("users") do + add_if_not_exists :keys, :text + end + end +end diff --git a/priv/repo/migrations/20191006135457_move_keys_to_separate_column.exs b/priv/repo/migrations/20191006135457_move_keys_to_separate_column.exs new file mode 100644 index 000000000..504dde53a --- /dev/null +++ b/priv/repo/migrations/20191006135457_move_keys_to_separate_column.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.MoveKeysToSeparateColumn do + use Ecto.Migration + + def change do + execute("update users set keys = info->>'keys' where local", "update users set info = jsonb_set(info, '{keys}'::text[], to_jsonb(keys)) where local") + end +end diff --git a/test/signature_test.exs b/test/signature_test.exs index d5bf63d7d..96c8ba07a 100644 --- a/test/signature_test.exs +++ b/test/signature_test.exs @@ -80,7 +80,7 @@ test "it returns signature headers" do user = insert(:user, %{ ap_id: "https://mastodon.social/users/lambadalambda", - info: %{keys: @private_key} + keys: @private_key }) assert Signature.sign( @@ -94,8 +94,7 @@ test "it returns signature headers" do end test "it returns error" do - user = - insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", info: %{keys: ""}}) + user = insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", keys: ""}) assert Signature.sign( user, diff --git a/test/user_test.exs b/test/user_test.exs index 1bc853c94..ae21286e4 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1457,15 +1457,15 @@ test "if user is unconfirmed" do describe "ensure_keys_present" do test "it creates keys for a user and stores them in info" do user = insert(:user) - refute is_binary(user.info.keys) + refute is_binary(user.keys) {:ok, user} = User.ensure_keys_present(user) - assert is_binary(user.info.keys) + assert is_binary(user.keys) end test "it doesn't create keys if there already are some" do - user = insert(:user, %{info: %{keys: "xxx"}}) + user = insert(:user, keys: "xxx") {:ok, user} = User.ensure_keys_present(user) - assert user.info.keys == "xxx" + assert user.keys == "xxx" end end From b93856874de673f1c05c557ad482d4480ca7e0a7 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sun, 6 Oct 2019 17:12:17 +0300 Subject: [PATCH 11/15] [#1234] Merge remote-tracking branch 'remotes/upstream/develop' into 1234-mastodon-2-4-3-oauth-scopes # Conflicts: # CHANGELOG.md # lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex # lib/pleroma/web/router.ex --- .../activity_pub/activity_pub_controller.ex | 5 - lib/pleroma/web/masto_fe_controller.ex | 12 + .../controllers/account_controller.ex | 10 +- .../controllers/app_controller.ex | 3 + .../controllers/domain_block_controller.ex | 2 + .../controllers/follow_request_controller.ex | 2 + .../controllers/mastodon_api_controller.ex | 63 ---- .../controllers/media_controller.ex | 5 + .../controllers/notification_controller.ex | 2 + .../controllers/poll_controller.ex | 10 + .../controllers/report_controller.ex | 2 + .../controllers/search_controller.ex | 2 + .../controllers/status_controller.ex | 5 + .../controllers/subscription_controller.ex | 2 + .../controllers/suggestion_controller.ex | 5 + .../controllers/emoji_api_controller.ex | 18 ++ lib/pleroma/web/router.ex | 285 +++++++----------- .../controllers/util_controller.ex | 2 + 18 files changed, 186 insertions(+), 249 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 93fd9e248..080030eb5 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -30,11 +30,6 @@ 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]) diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex index ac9af7502..87860f1d5 100644 --- a/lib/pleroma/web/masto_fe_controller.ex +++ b/lib/pleroma/web/masto_fe_controller.ex @@ -5,8 +5,20 @@ defmodule Pleroma.Web.MastoFEController do use Pleroma.Web, :controller + 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( + OAuthScopesPlug, + %{scopes: ["read"], fallback: :proceed_unauthenticated, skip_instance_privacy_check: true} + when action == :index + ) + + plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index) + @doc "GET /web/*path" def index(%{assigns: %{user: user}} = conn, _params) do token = get_session(conn, :oauth_token) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index e195f56c4..9ef7fd48d 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -36,6 +36,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists) + plug( + OAuthScopesPlug, + %{scopes: ["follow", "read:blocks"]} when action == :blocks + ) + plug( OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock] @@ -43,11 +48,14 @@ 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 [:follow, :unfollow] + %{scopes: ["follow", "write:follows"]} when action in [:follows, :follow, :unfollow] ) + plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes) + plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute]) plug( diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex index abbe16a88..13a30a34d 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.OAuthScopesPlug alias Pleroma.Repo alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Scopes @@ -12,6 +13,8 @@ defmodule Pleroma.Web.MastodonAPI.AppController do action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials) + @local_mastodon_name "Mastodon-Local" @doc "POST /api/v1/apps" 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 45c5ef8a4..c7606246b 100644 --- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex @@ -18,6 +18,8 @@ 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: %{info: info}}} = conn, _) do json(conn, Map.get(info, :domain_blocks, [])) 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 06672e2bb..3ccbdf1c6 100644 --- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex @@ -21,6 +21,8 @@ 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/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex index 32077d420..7d839a8cf 100644 --- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -7,69 +7,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do require Logger - alias Pleroma.Plugs.OAuthScopesPlug - @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"]} when action == :put_settings) - - plug( - OAuthScopesPlug, - %{@unauthenticated_access | scopes: ["read:statuses"]} when action == :get_poll - ) - - plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :poll_vote) - - plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites) - - plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action in [:upload, :update_media]) - - plug( - OAuthScopesPlug, - %{scopes: ["follow", "read:blocks"]} when action == :blocks - ) - - # To do: POST /api/v1/follows is not present in Mastodon; consider removing the action - plug( - OAuthScopesPlug, - %{scopes: ["follow", "write:follows"]} when action == :follows - ) - - plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes) - - # Note: scope not present in Mastodon: read:bookmarks - plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks) - - # An extra safety measure for possible actions not guarded by OAuth permissions specification - plug( - Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - when action not in [ - :create_app, - :index, - :login, - :logout, - :password_reset, - :masto_instance, - :peers, - :custom_emojis - ] - ) - - plug(RateLimiter, :password_reset when action == :password_reset) - - @local_mastodon_name "Mastodon-Local" - action_fallback(Pleroma.Web.MastodonAPI.FallbackController) # Stubs for unimplemented mastodon api diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 57a5b60fb..ed4c08d99 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -6,12 +6,17 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do use Pleroma.Web, :controller alias Pleroma.Object + alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub action_fallback(Pleroma.Web.MastodonAPI.FallbackController) plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) + 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 36c6defc2..16759be6a 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -20,6 +20,8 @@ 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(%{assigns: %{user: user}} = conn, params) do notifications = MastodonAPI.get_notifications(user, params) diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex index fbf7f8673..d129f8672 100644 --- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex @@ -9,11 +9,21 @@ defmodule Pleroma.Web.MastodonAPI.PollController do alias Pleroma.Activity alias Pleroma.Object + alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + plug( + OAuthScopesPlug, + %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} when action == :show + ) + + 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 313f885a6..263c2180f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex @@ -11,6 +11,8 @@ 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/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 9f39b00f8..6cfd68a84 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -20,6 +20,8 @@ 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) + plug(RateLimiter, :search when action in [:search, :search2, :account_search]) def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 2cbf33046..0c16e9b0f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -47,6 +47,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do ] ) + plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites) + plug( OAuthScopesPlug, %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite] @@ -65,6 +67,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin]) + # Note: scope not present in Mastodon: read:bookmarks + plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks) + # Note: scope not present in Mastodon: write:bookmarks plug( OAuthScopesPlug, diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex index 287eebf92..fc7d52824 100644 --- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex @@ -14,6 +14,8 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]}) + plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + # Creates PushSubscription # POST /api/v1/push/subscription # diff --git a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex index 9076bb849..fe71c36af 100644 --- a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex @@ -8,11 +8,16 @@ defmodule Pleroma.Web.MastodonAPI.SuggestionController do require Logger alias Pleroma.Config + alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.MediaProxy action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :index) + + plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + @doc "GET /api/v1/suggestions" def index(%{assigns: %{user: user}} = conn, _) do if Config.get([:suggestions, :enabled], false) do 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 545ad80c9..a474d41d4 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -1,8 +1,26 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do use Pleroma.Web, :controller + alias Pleroma.Plugs.OAuthScopesPlug + require Logger + plug( + OAuthScopesPlug, + %{scopes: ["write"]} + when action in [ + :create, + :delete, + :download_from, + :list_from, + :import_from_fs, + :update_file, + :update_metadata + ] + ) + + plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + def emoji_dir_path do Path.join( Pleroma.Config.get!([:instance, :static_dir]), diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index a36c40a3b..675f485b2 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -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) @@ -213,7 +188,7 @@ defmodule Pleroma.Web.Router do scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do scope "/packs" do # Modifying packs - pipe_through([:admin_api, :oauth_write]) + pipe_through(:admin_api) post("/import_from_fs", EmojiAPIController, :import_from_fs) @@ -238,31 +213,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) - 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 - - 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 @@ -289,14 +253,14 @@ defmodule Pleroma.Web.Router do scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do scope [] do pipe_through(:authenticated_api) - pipe_through(:oauth_read) + get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses) get("/conversations/:id", PleromaAPIController, :conversation) end scope [] do pipe_through(:authenticated_api) - pipe_through(:oauth_write) + patch("/conversations/:id", PleromaAPIController, :update_conversation) post("/notifications/read", PleromaAPIController, :read_notification) @@ -312,13 +276,11 @@ defmodule Pleroma.Web.Router do scope [] do pipe_through(:api) - pipe_through(:oauth_read_or_public) get("/accounts/:id/favourites", AccountController, :favourites) end scope [] do pipe_through(:authenticated_api) - pipe_through(:oauth_follow) post("/accounts/:id/subscribe", AccountController, :subscribe) post("/accounts/:id/unsubscribe", AccountController, :unsubscribe) @@ -328,131 +290,114 @@ defmodule Pleroma.Web.Router do end scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do - pipe_through([:api, :oauth_read_or_public]) - + pipe_through(:api) get("/accounts/:id/scrobbles", ScrobbleController, :user_scrobbles) end scope "/api/v1", Pleroma.Web.MastodonAPI do pipe_through(:authenticated_api) - scope [] do - pipe_through(:oauth_read) + get("/accounts/verify_credentials", AccountController, :verify_credentials) - get("/accounts/verify_credentials", AccountController, :verify_credentials) + get("/accounts/relationships", AccountController, :relationships) - get("/accounts/relationships", AccountController, :relationships) + get("/accounts/:id/lists", AccountController, :lists) + get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array) - get("/accounts/:id/lists", AccountController, :lists) - get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array) + get("/follow_requests", FollowRequestController, :index) + get("/blocks", AccountController, :blocks) + get("/mutes", AccountController, :mutes) - get("/follow_requests", FollowRequestController, :index) - get("/blocks", AccountController, :blocks) - get("/mutes", AccountController, :mutes) + get("/timelines/home", TimelineController, :home) + get("/timelines/direct", TimelineController, :direct) - get("/timelines/home", TimelineController, :home) - get("/timelines/direct", TimelineController, :direct) + get("/favourites", StatusController, :favourites) + get("/bookmarks", StatusController, :bookmarks) - get("/favourites", StatusController, :favourites) - get("/bookmarks", StatusController, :bookmarks) + get("/notifications", NotificationController, :index) + get("/notifications/:id", NotificationController, :show) + post("/notifications/clear", NotificationController, :clear) + post("/notifications/dismiss", NotificationController, :dismiss) + delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple) - get("/notifications", NotificationController, :index) - get("/notifications/:id", NotificationController, :show) - post("/notifications/clear", NotificationController, :clear) - post("/notifications/dismiss", NotificationController, :dismiss) - delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple) + get("/scheduled_statuses", ScheduledActivityController, :index) + get("/scheduled_statuses/:id", ScheduledActivityController, :show) - 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("/lists", ListController, :index) - get("/lists/:id", ListController, :show) - get("/lists/:id/accounts", ListController, :list_accounts) + get("/domain_blocks", DomainBlockController, :index) - get("/domain_blocks", DomainBlockController, :index) + get("/filters", FilterController, :index) - get("/filters", FilterController, :index) + get("/suggestions", SuggestionController, :index) - get("/suggestions", SuggestionController, :index) + get("/conversations", ConversationController, :index) + post("/conversations/:id/read", ConversationController, :read) - get("/conversations", ConversationController, :index) - post("/conversations/:id/read", ConversationController, :read) + get("/endorsements", AccountController, :endorsements) - get("/endorsements", MastodonAPIController, :empty_array) - end + patch("/accounts/update_credentials", AccountController, :update_credentials) - scope [] do - pipe_through(:oauth_write) + post("/statuses", StatusController, :create) + delete("/statuses/:id", StatusController, :delete) - patch("/accounts/update_credentials", AccountController, :update_credentials) + post("/statuses/:id/reblog", StatusController, :reblog) + post("/statuses/:id/unreblog", StatusController, :unreblog) + post("/statuses/:id/favourite", StatusController, :favourite) + post("/statuses/:id/unfavourite", StatusController, :unfavourite) + post("/statuses/:id/pin", StatusController, :pin) + post("/statuses/:id/unpin", StatusController, :unpin) + post("/statuses/:id/bookmark", StatusController, :bookmark) + post("/statuses/:id/unbookmark", StatusController, :unbookmark) + post("/statuses/:id/mute", StatusController, :mute_conversation) + post("/statuses/:id/unmute", StatusController, :unmute_conversation) - post("/statuses", StatusController, :create) - delete("/statuses/:id", StatusController, :delete) + put("/scheduled_statuses/:id", ScheduledActivityController, :update) + delete("/scheduled_statuses/:id", ScheduledActivityController, :delete) - post("/statuses/:id/reblog", StatusController, :reblog) - post("/statuses/:id/unreblog", StatusController, :unreblog) - post("/statuses/:id/favourite", StatusController, :favourite) - post("/statuses/:id/unfavourite", StatusController, :unfavourite) - post("/statuses/:id/pin", StatusController, :pin) - post("/statuses/:id/unpin", StatusController, :unpin) - post("/statuses/:id/bookmark", StatusController, :bookmark) - post("/statuses/:id/unbookmark", StatusController, :unbookmark) - post("/statuses/:id/mute", StatusController, :mute_conversation) - post("/statuses/:id/unmute", StatusController, :unmute_conversation) + post("/polls/:id/votes", PollController, :vote) - put("/scheduled_statuses/:id", ScheduledActivityController, :update) - delete("/scheduled_statuses/:id", ScheduledActivityController, :delete) + post("/media", MediaController, :create) + put("/media/:id", MediaController, :update) - post("/polls/:id/votes", PollController, :vote) + delete("/lists/:id", ListController, :delete) + post("/lists", ListController, :create) + put("/lists/:id", ListController, :update) - post("/media", MediaController, :create) - put("/media/:id", MediaController, :update) + post("/lists/:id/accounts", ListController, :add_to_list) + delete("/lists/:id/accounts", ListController, :remove_from_list) - delete("/lists/:id", ListController, :delete) - post("/lists", ListController, :create) - put("/lists/:id", ListController, :update) + post("/filters", FilterController, :create) + get("/filters/:id", FilterController, :show) + put("/filters/:id", FilterController, :update) + delete("/filters/:id", FilterController, :delete) - post("/lists/:id/accounts", ListController, :add_to_list) - delete("/lists/:id/accounts", ListController, :remove_from_list) + post("/reports", ReportController, :create) - post("/filters", FilterController, :create) - get("/filters/:id", FilterController, :show) - put("/filters/:id", FilterController, :update) - delete("/filters/:id", FilterController, :delete) + 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("/reports", ReportController, :create) - end + post("/follow_requests/:id/authorize", FollowRequestController, :authorize) + post("/follow_requests/:id/reject", FollowRequestController, :reject) - scope [] do - pipe_through(:oauth_follow) + post("/domain_blocks", DomainBlockController, :create) + delete("/domain_blocks", DomainBlockController, :delete) - 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) - end - - scope [] do - pipe_through(:oauth_push) - - 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 do - pipe_through([:authenticated_api, :oauth_write]) + pipe_through(:authenticated_api) put("/settings", MastoFEController, :put_settings) end @@ -477,30 +422,26 @@ defmodule Pleroma.Web.Router do get("/trends", MastodonAPIController, :empty_array) - scope [] do - pipe_through(:oauth_read_or_public) + get("/timelines/public", TimelineController, :public) + get("/timelines/tag/:tag", TimelineController, :hashtag) + get("/timelines/list/:list_id", TimelineController, :list) - 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("/statuses", StatusController, :index) - get("/statuses/:id", StatusController, :show) - get("/statuses/:id/context", StatusController, :context) + get("/polls/:id", PollController, :show) - 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("/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 + get("/search", SearchController, :search) end scope "/api/v2", Pleroma.Web.MastodonAPI do - pipe_through([:api, :oauth_read_or_public]) + pipe_through(:api) get("/search", SearchController, :search2) end @@ -531,11 +472,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 @@ -599,23 +536,14 @@ 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 + get("/api/ap/whoami", ActivityPubController, :whoami) + get("/users/:nickname/inbox", ActivityPubController, :read_inbox) - scope [] do - pipe_through(:oauth_write) - post("/users/:nickname/outbox", ActivityPubController, :update_outbox) - post("/api/ap/upload_media", ActivityPubController, :upload_media) - end + post("/users/:nickname/outbox", ActivityPubController, :update_outbox) + post("/api/ap/upload_media", ActivityPubController, :upload_media) - scope [] do - pipe_through(:oauth_read_or_public) - get("/users/:nickname/followers", ActivityPubController, :followers) - get("/users/:nickname/following", ActivityPubController, :following) - end + get("/users/:nickname/followers", ActivityPubController, :followers) + get("/users/:nickname/following", ActivityPubController, :following) end scope "/", Pleroma.Web.ActivityPub do @@ -665,10 +593,7 @@ defmodule Pleroma.Web.Router do post("/auth/password", MastodonAPI.AuthController, :password_reset) - scope [] do - pipe_through(:oauth_read) - get("/web/*path", MastoFEController, :index) - end + get("/web/*path", MastoFEController, :index) end pipeline :remote_media do diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index c84359ddb..2305bb413 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -39,6 +39,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do ] ) + plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read) + plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version]) def help_test(conn, _params) do From 1d8e956c32e412283aed0cebf90df11121d897bb Mon Sep 17 00:00:00 2001 From: feld Date: Sun, 6 Oct 2019 14:13:04 +0000 Subject: [PATCH 12/15] Use the user.id instead of nickname in report URLs Ensures links to profiles of remote users work. --- CHANGELOG.md | 1 + lib/pleroma/emails/admin_email.ex | 2 +- test/emails/admin_email_test.exs | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db505591b..607643b7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`) - Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname` - Added `:instance, extended_nickname_format` setting to the default config +- Report emails now include functional links to profiles of remote user accounts ## [1.1.0] - 2019-??-?? ### Security diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex index c14be02dd..5a0903c13 100644 --- a/lib/pleroma/emails/admin_email.ex +++ b/lib/pleroma/emails/admin_email.ex @@ -17,7 +17,7 @@ defp instance_notify_email do end defp user_url(user) do - Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, user.nickname) + Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, user.id) end def report(to, reporter, account, statuses, comment) do diff --git a/test/emails/admin_email_test.exs b/test/emails/admin_email_test.exs index 31eac5f12..02c277a33 100644 --- a/test/emails/admin_email_test.exs +++ b/test/emails/admin_email_test.exs @@ -19,8 +19,8 @@ test "build report email" do AdminEmail.report(to_user, reporter, account, [%{name: "Test", id: "12"}], "Test comment") status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, "12") - reporter_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, reporter.nickname) - account_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, account.nickname) + reporter_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, reporter.id) + account_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, account.id) assert res.to == [{to_user.name, to_user.email}] assert res.from == {config[:name], config[:notify_email]} From 125f96e7064aa0d714c91bf4a0c78c25734c64ef Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 6 Oct 2019 17:05:51 +0300 Subject: [PATCH 13/15] Fix the use of queries with a schema in a migration --- .../20190711042024_copy_muted_to_muted_notifications.exs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs b/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs index 50669902e..815d66549 100644 --- a/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs +++ b/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs @@ -1,15 +1,10 @@ defmodule Pleroma.Repo.Migrations.CopyMutedToMutedNotifications do use Ecto.Migration + import Ecto.Query alias Pleroma.User def change do - query = - User.Query.build(%{ - local: true, - active: true, - order_by: :id - }) - + query = from(u in "users", where: fragment("not (?->'deactivated' @> 'true')", u.info), select: %{info: u.info}, where: u.local == true, order_by: u.id) Pleroma.Repo.stream(query) |> Enum.each(fn %{info: %{mutes: mutes} = info} = user -> From ee88afb2e2d26defe18541dd2a57f8c34e191cb8 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 6 Oct 2019 17:53:03 +0300 Subject: [PATCH 14/15] Fix muted notification migration only working with a schema Should also improve performance. I tested it on my local DB, but if anyone has a backup of <=1.0 db they can test this on, please do --- ...0711042024_copy_muted_to_muted_notifications.exs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs b/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs index 815d66549..b717cab2e 100644 --- a/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs +++ b/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs @@ -1,19 +1,8 @@ defmodule Pleroma.Repo.Migrations.CopyMutedToMutedNotifications do use Ecto.Migration - import Ecto.Query alias Pleroma.User def change do - query = from(u in "users", where: fragment("not (?->'deactivated' @> 'true')", u.info), select: %{info: u.info}, where: u.local == true, order_by: u.id) - Pleroma.Repo.stream(query) - |> Enum.each(fn - %{info: %{mutes: mutes} = info} = user -> - info_cng = - Ecto.Changeset.cast(info, %{muted_notifications: mutes}, [:muted_notifications]) - - Ecto.Changeset.change(user) - |> Ecto.Changeset.put_embed(:info, info_cng) - |> Pleroma.Repo.update() - end) + execute("update users set info = jsonb_set(info, '{muted_notifications}', info->'mutes', true) where local = true") end end From 58aeabd020ae7355aea6b63a331b164e8b23a6c0 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Mon, 7 Oct 2019 02:55:09 +0200 Subject: [PATCH 15/15] mrf/simple_policy: check actor against accept/reject --- CHANGELOG.md | 1 + .../web/activity_pub/mrf/simple_policy.ex | 4 ++- .../activity_pub/mrf/simple_policy_test.exs | 26 +++++++++++++++---- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 607643b7c..33321e990 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Admin API: Return `total` when querying for reports - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`) - Admin API: Return link alongside with token on password reset +- MRF (Simple Policy): Also use `:accept`/`:reject` on the actors rather than only their activities ### Fixed - Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`) diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index 8aa6852f0..8e53296e7 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -168,7 +168,9 @@ def filter(%{"id" => actor, "type" => obj_type} = object) when obj_type in ["Application", "Group", "Organization", "Person", "Service"] do actor_info = URI.parse(actor) - with {:ok, object} <- check_avatar_removal(actor_info, object), + with {:ok, object} <- check_accept(actor_info, object), + {:ok, object} <- check_reject(actor_info, object), + {:ok, object} <- check_avatar_removal(actor_info, object), {:ok, object} <- check_banner_removal(actor_info, object) do {:ok, object} else diff --git a/test/web/activity_pub/mrf/simple_policy_test.exs b/test/web/activity_pub/mrf/simple_policy_test.exs index 7203b27da..df0f223f8 100644 --- a/test/web/activity_pub/mrf/simple_policy_test.exs +++ b/test/web/activity_pub/mrf/simple_policy_test.exs @@ -236,7 +236,7 @@ test "is empty" do assert SimplePolicy.filter(remote_message) == {:ok, remote_message} end - test "has a matching host" do + test "activity has a matching host" do Config.put([:mrf_simple, :reject], ["remote.instance"]) remote_message = build_remote_message() @@ -244,13 +244,21 @@ test "has a matching host" do assert SimplePolicy.filter(remote_message) == {:reject, nil} end - test "match with wildcard domain" do + test "activity matches with wildcard domain" do Config.put([:mrf_simple, :reject], ["*.remote.instance"]) remote_message = build_remote_message() assert SimplePolicy.filter(remote_message) == {:reject, nil} end + + test "actor has a matching host" do + Config.put([:mrf_simple, :reject], ["remote.instance"]) + + remote_user = build_remote_user() + + assert SimplePolicy.filter(remote_user) == {:reject, nil} + end end describe "when :accept" do @@ -264,7 +272,7 @@ test "is empty" do assert SimplePolicy.filter(remote_message) == {:ok, remote_message} end - test "is not empty but it doesn't have a matching host" do + test "is not empty but activity doesn't have a matching host" do Config.put([:mrf_simple, :accept], ["non.matching.remote"]) local_message = build_local_message() @@ -274,7 +282,7 @@ test "is not empty but it doesn't have a matching host" do assert SimplePolicy.filter(remote_message) == {:reject, nil} end - test "has a matching host" do + test "activity has a matching host" do Config.put([:mrf_simple, :accept], ["remote.instance"]) local_message = build_local_message() @@ -284,7 +292,7 @@ test "has a matching host" do assert SimplePolicy.filter(remote_message) == {:ok, remote_message} end - test "match with wildcard domain" do + test "activity matches with wildcard domain" do Config.put([:mrf_simple, :accept], ["*.remote.instance"]) local_message = build_local_message() @@ -293,6 +301,14 @@ test "match with wildcard domain" do assert SimplePolicy.filter(local_message) == {:ok, local_message} assert SimplePolicy.filter(remote_message) == {:ok, remote_message} end + + test "actor has a matching host" do + Config.put([:mrf_simple, :accept], ["remote.instance"]) + + remote_user = build_remote_user() + + assert SimplePolicy.filter(remote_user) == {:ok, remote_user} + end end describe "when :avatar_removal" do