Merge branch 'develop' into fix/signup-without-email
authorEgor Kislitsyn <egor@kislitsyn.com>
Mon, 2 Mar 2020 11:35:49 +0000 (15:35 +0400)
committerEgor Kislitsyn <egor@kislitsyn.com>
Mon, 2 Mar 2020 11:35:49 +0000 (15:35 +0400)
1  2 
CHANGELOG.md
lib/pleroma/user.ex
test/web/mastodon_api/controllers/account_controller_test.exs

diff --combined CHANGELOG.md
index a924d4083c21443ed201d02716653026e9fb5aee,37df345ede5ad12372fe7f5dab7e9d7f459be8de..d058370161f09c0e2dc2039815342835588def43
@@@ -4,6 -4,9 +4,9 @@@ All notable changes to this project wil
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
  
  ## [Unreleased]
+ ### Security
+ - Mastodon API: Fix being able to request enourmous amount of statuses in timelines leading to DoS. Now limited to 40 per request.
  ### Removed
  - **Breaking**: Removed 1.0+ deprecated configurations `Pleroma.Upload, :strip_exif` and `:instance, :dedupe_media`
  - **Breaking**: OStatus protocol support
@@@ -35,7 -38,6 +38,7 @@@
  - Rate limiter is now disabled for localhost/socket (unless remoteip plug is enabled)
  - Logger: default log level changed from `warn` to `info`.
  - Config mix task `migrate_to_db` truncates `config` table before migrating the config file.
 +- Allow account registration without an email
  <details>
    <summary>API Changes</summary>
  
@@@ -57,6 -59,7 +60,7 @@@
  - Admin API: Render whole status in grouped reports
  - Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
  - Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try.
+ - Mastodon API: Limit timeline requests to 3 per timeline per 500ms per user/ip by default.
  </details>
  
  ### Added
  - Configuration: `feed` option for user atom feed.
  - Pleroma API: Add Emoji reactions
  - Admin API: Add `/api/pleroma/admin/instances/:instance/statuses` - lists all statuses from a given instance
+ - Admin API: Add `/api/pleroma/admin/users/:nickname/statuses` - lists all statuses from a given user
  - Admin API: `PATCH /api/pleroma/users/confirm_email` to confirm email for multiple users, `PATCH /api/pleroma/users/resend_confirmation_email` to resend confirmation email for multiple users
  - ActivityPub: Configurable `type` field of the actors.
  - Mastodon API: `/api/v1/accounts/:id` has `source/pleroma/actor_type` field.
  - Pleroma API: Add reactions for a single emoji.
  - ActivityPub: `[:activitypub, :note_replies_output_limit]` setting sets the number of note self-replies to output on outgoing federation.
  - Admin API: `GET /api/pleroma/admin/stats` to get status count by visibility scope
+ - Admin API: `GET /api/pleroma/admin/statuses` - list all statuses (accepts `godmode` and `local_only`)
  </details>
  
  ### Fixed
diff --combined lib/pleroma/user.ex
index 5271d8dbeb975865b453f17283c821a29e2e5282,5fe79333e61d229ec4d4053a9920e20ee4c3b391..7531757f5b53e18ab968bea682b0eec24ddeea17
@@@ -530,14 -530,7 +530,14 @@@ defmodule Pleroma.User d
    end
  
    def maybe_validate_required_email(changeset, true), do: changeset
 -  def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email])
 +
 +  def maybe_validate_required_email(changeset, _) do
 +    if Pleroma.Config.get([:instance, :account_activation_required]) do
 +      validate_required(changeset, [:email])
 +    else
 +      changeset
 +    end
 +  end
  
    defp put_ap_id(changeset) do
      ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
      Cachex.del(:user_cache, "nickname:#{user.nickname}")
    end
  
+   @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
    def get_cached_by_ap_id(ap_id) do
      key = "ap_id:#{ap_id}"
-     Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
+     with {:ok, nil} <- Cachex.get(:user_cache, key),
+          user when not is_nil(user) <- get_by_ap_id(ap_id),
+          {:ok, true} <- Cachex.put(:user_cache, key, user) do
+       user
+     else
+       {:ok, user} -> user
+       nil -> nil
+     end
    end
  
    def get_cached_by_id(id) do
index ff7cb88d10234661848f6c6d6b3e58d92969dfc1,57d0f4416529805e80ecc8a1148709b0f7d33153..7f7d8cea383a11dab20ddfe5cd8713b4e793a622
@@@ -601,8 -601,6 +601,8 @@@ defmodule Pleroma.Web.MastodonAPI.Accou
        [valid_params: valid_params]
      end
  
 +    clear_config([:instance, :account_activation_required])
 +
      test "Account registration via Application", %{conn: conn} do
        conn =
          post(conn, "/api/v1/apps", %{
        assert json_response(res, 400) == %{"error" => "{\"email\":[\"has already been taken\"]}"}
      end
  
-     clear_config([Pleroma.Plugs.RemoteIp, :enabled])
-     test "rate limit", %{conn: conn} do
-       Pleroma.Config.put([Pleroma.Plugs.RemoteIp, :enabled], true)
-       app_token = insert(:oauth_token, user: nil)
-       conn =
-         conn
-         |> put_req_header("authorization", "Bearer " <> app_token.token)
-         |> Map.put(:remote_ip, {15, 15, 15, 15})
-       for i <- 1..5 do
-         conn =
-           post(conn, "/api/v1/accounts", %{
-             username: "#{i}lain",
-             email: "#{i}lain@example.org",
-             password: "PlzDontHackLain",
-             agreement: true
-           })
-         %{
-           "access_token" => token,
-           "created_at" => _created_at,
-           "scope" => _scope,
-           "token_type" => "Bearer"
-         } = json_response(conn, 200)
-         token_from_db = Repo.get_by(Token, token: token)
-         assert token_from_db
-         token_from_db = Repo.preload(token_from_db, :user)
-         assert token_from_db.user
-         assert token_from_db.user.confirmation_pending
-       end
-       conn =
-         post(conn, "/api/v1/accounts", %{
-           username: "6lain",
-           email: "6lain@example.org",
-           password: "PlzDontHackLain",
-           agreement: true
-         })
-       assert json_response(conn, :too_many_requests) == %{"error" => "Throttled"}
-     end
      test "returns bad_request if missing required params", %{
        conn: conn,
        valid_params: valid_params
        assert json_response(res, 200)
  
        [{127, 0, 0, 1}, {127, 0, 0, 2}, {127, 0, 0, 3}, {127, 0, 0, 4}]
 -      |> Stream.zip(valid_params)
 +      |> Stream.zip(Map.delete(valid_params, :email))
        |> Enum.each(fn {ip, {attr, _}} ->
          res =
            conn
        end)
      end
  
++    clear_config([:instance, :account_activation_required])
++
 +    test "returns bad_request if missing email params when :account_activation_required is enabled",
 +         %{conn: conn, valid_params: valid_params} do
 +      Pleroma.Config.put([:instance, :account_activation_required], true)
 +
 +      app_token = insert(:oauth_token, user: nil)
 +      conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token)
 +
 +      res =
 +        conn
 +        |> Map.put(:remote_ip, {127, 0, 0, 5})
 +        |> post("/api/v1/accounts", Map.delete(valid_params, :email))
 +
 +      assert json_response(res, 400) == %{"error" => "Missing parameters"}
 +
 +      res =
 +        conn
 +        |> Map.put(:remote_ip, {127, 0, 0, 6})
 +        |> post("/api/v1/accounts", Map.put(valid_params, :email, ""))
 +
 +      assert json_response(res, 400) == %{"error" => "{\"email\":[\"can't be blank\"]}"}
 +    end
 +
 +    test "allow registration without an email", %{conn: conn, valid_params: valid_params} do
 +      app_token = insert(:oauth_token, user: nil)
 +      conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token)
 +
 +      res =
 +        conn
 +        |> Map.put(:remote_ip, {127, 0, 0, 7})
 +        |> post("/api/v1/accounts", Map.delete(valid_params, :email))
 +
 +      assert json_response(res, 200)
 +    end
 +
 +    test "allow registration with an empty email", %{conn: conn, valid_params: valid_params} do
 +      app_token = insert(:oauth_token, user: nil)
 +      conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token)
 +
 +      res =
 +        conn
 +        |> Map.put(:remote_ip, {127, 0, 0, 8})
 +        |> post("/api/v1/accounts", Map.put(valid_params, :email, ""))
 +
 +      assert json_response(res, 200)
 +    end
 +
      test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_params} do
        conn = put_req_header(conn, "authorization", "Bearer " <> "invalid-token")
  
      end
    end
  
+   describe "create account by app / rate limit" do
+     clear_config([Pleroma.Plugs.RemoteIp, :enabled]) do
+       Pleroma.Config.put([Pleroma.Plugs.RemoteIp, :enabled], true)
+     end
+     clear_config([:rate_limit, :app_account_creation]) do
+       Pleroma.Config.put([:rate_limit, :app_account_creation], {10_000, 2})
+     end
+     test "respects rate limit setting", %{conn: conn} do
+       app_token = insert(:oauth_token, user: nil)
+       conn =
+         conn
+         |> put_req_header("authorization", "Bearer " <> app_token.token)
+         |> Map.put(:remote_ip, {15, 15, 15, 15})
+       for i <- 1..2 do
+         conn =
+           post(conn, "/api/v1/accounts", %{
+             username: "#{i}lain",
+             email: "#{i}lain@example.org",
+             password: "PlzDontHackLain",
+             agreement: true
+           })
+         %{
+           "access_token" => token,
+           "created_at" => _created_at,
+           "scope" => _scope,
+           "token_type" => "Bearer"
+         } = json_response(conn, 200)
+         token_from_db = Repo.get_by(Token, token: token)
+         assert token_from_db
+         token_from_db = Repo.preload(token_from_db, :user)
+         assert token_from_db.user
+         assert token_from_db.user.confirmation_pending
+       end
+       conn =
+         post(conn, "/api/v1/accounts", %{
+           username: "6lain",
+           email: "6lain@example.org",
+           password: "PlzDontHackLain",
+           agreement: true
+         })
+       assert json_response(conn, :too_many_requests) == %{"error" => "Throttled"}
+     end
+   end
    describe "GET /api/v1/accounts/:id/lists - account_lists" do
      test "returns lists to which the account belongs" do
        %{user: user, conn: conn} = oauth_access(["read:lists"])