Merge branch 'features/mastoapi/2.7.0-registration' into 'develop'
authorkaniini <nenolod@gmail.com>
Mon, 13 May 2019 18:35:45 +0000 (18:35 +0000)
committerkaniini <nenolod@gmail.com>
Mon, 13 May 2019 18:35:45 +0000 (18:35 +0000)
Features/mastoapi/2.7.0 registration

Closes #773

See merge request pleroma/pleroma!1134

27 files changed:
CHANGELOG.md
config/config.exs
docs/api/differences_in_mastoapi_responses.md
docs/config.md
lib/mix/tasks/pleroma/user.ex
lib/pleroma/plugs/oauth_plug.ex
lib/pleroma/plugs/rate_limit_plug.ex [new file with mode: 0644]
lib/pleroma/user.ex
lib/pleroma/user/info.ex
lib/pleroma/web/admin_api/admin_api_controller.ex
lib/pleroma/web/auth/pleroma_authenticator.ex
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
lib/pleroma/web/oauth/app.ex
lib/pleroma/web/oauth/authorization.ex
lib/pleroma/web/oauth/oauth_controller.ex
lib/pleroma/web/oauth/token.ex
lib/pleroma/web/router.ex
lib/pleroma/web/twitter_api/twitter_api.ex
lib/pleroma/web/twitter_api/twitter_api_controller.ex
mix.exs
mix.lock
test/plugs/rate_limit_plug_test.exs [new file with mode: 0644]
test/user_test.exs
test/web/mastodon_api/mastodon_api_controller_test.exs
test/web/oauth/oauth_controller_test.exs
test/web/twitter_api/twitter_api_controller_test.exs
test/web/views/error_view_test.exs

index 76e6f6b394f7ce3111f1a20a6864d9f76ed4316a..cb934dab4a3407b3b526d81a5994b6e9b4387d64 100644 (file)
@@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension)
 - Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension)
 - Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/)
+- Mastodon API: REST API for creating an account
 - ActivityPub C2S: OAuth endpoints
 - Metadata RelMe provider
 - OAuth: added support for refresh tokens
@@ -57,10 +58,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Mastodon API: Add `with_muted` parameter to timeline endpoints
 - Mastodon API: Actual reblog hiding instead of a dummy
 - Mastodon API: Remove attachment limit in the Status entity
+- Mastodon API: Added support max_id & since_id for bookmark timeline endpoints.
 - Deps: Updated Cowboy to 2.6
 - Deps: Updated Ecto to 3.0.7
 - Don't ship finmoji by default, they can be installed as an emoji pack
-- Mastodon API: Added support max_id & since_id for bookmark timeline endpoints.
 - Admin API: Move the user related API to `api/pleroma/admin/users`
 
 ### Fixed
index 1e64b79a7a043ed4b4194c23e4910a7793c6e003..e8aad855c3f80ce4c49318ba7d774a11825c2b62 100644 (file)
@@ -234,6 +234,8 @@ config :pleroma, :instance,
   safe_dm_mentions: false,
   healthcheck: false
 
+config :pleroma, :app_account_creation, enabled: false, max_requests: 5, interval: 1800
+
 config :pleroma, :markup,
   # XXX - unfortunately, inline images must be enabled by default right now, because
   # of custom emoji.  Issue #275 discusses defanging that somehow.
index d3ba41b6a1964e2f68b009300c3e831a821a7aa0..36b47608e1655913e3cf8d127b11a4067b7113f1 100644 (file)
@@ -87,3 +87,13 @@ Additional parameters can be added to the JSON body/Form data:
 
 `POST /oauth/token`
 Post here request with grant_type=refresh_token to obtain new access token. Returns an access token.
+
+## Account Registration
+`POST /api/v1/accounts`
+
+Has theses additionnal parameters (which are the same as in Pleroma-API):
+    * `fullname`: optional
+    * `bio`: optional
+    * `captcha_solution`: optional, contains provider-specific captcha solution,
+    * `captcha_token`: optional, contains provider-specific captcha token
+    * `token`: invite token required when the registerations aren't public.
index 43ea24d80f1813332f5d5fb2a23210384bcece65..470f71b7cf71c9c289aedec2d9573be878031966 100644 (file)
@@ -105,6 +105,12 @@ config :pleroma, Pleroma.Emails.Mailer,
 * `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). (Default: `false`)
 * `healthcheck`: if set to true, system data will be shown on ``/api/pleroma/healthcheck``.
 
+## :app_account_creation
+REST API for creating an account settings
+* `enabled`: Enable/disable registration
+* `max_requests`: Number of requests allowed for creating accounts
+* `interval`: Interval for restricting requests for one ip (seconds)
+
 ## :logger
 * `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack
 
index 6a83a8c0d9669e1ea45eac1bfc3e33538a37e849..d130ff8c960e3ba919514490ffb2f22a2b7f35a2 100644 (file)
@@ -138,7 +138,7 @@ defmodule Mix.Tasks.Pleroma.User do
         bio: bio
       }
 
-      changeset = User.register_changeset(%User{}, params, confirmed: true)
+      changeset = User.register_changeset(%User{}, params, need_confirmation: false)
       {:ok, _user} = User.register(changeset)
 
       Mix.shell().info("User #{nickname} created")
index 9d43732eb43592cbafe41c6f2a08c947836ee44c..86bc4aa3a40db3e54e1125542504d5eac5216258 100644 (file)
@@ -8,6 +8,7 @@ defmodule Pleroma.Plugs.OAuthPlug do
 
   alias Pleroma.Repo
   alias Pleroma.User
+  alias Pleroma.Web.OAuth.App
   alias Pleroma.Web.OAuth.Token
 
   @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i")
@@ -22,18 +23,39 @@ defmodule Pleroma.Plugs.OAuthPlug do
       |> assign(:token, token_record)
       |> assign(:user, user)
     else
-      _ -> conn
+      _ ->
+        # token found, but maybe only with app
+        with {:ok, app, token_record} <- fetch_app_and_token(access_token) do
+          conn
+          |> assign(:token, token_record)
+          |> assign(:app, app)
+        else
+          _ -> conn
+        end
     end
   end
 
   def call(conn, _) do
-    with {:ok, token_str} <- fetch_token_str(conn),
-         {:ok, user, token_record} <- fetch_user_and_token(token_str) do
-      conn
-      |> assign(:token, token_record)
-      |> assign(:user, user)
-    else
-      _ -> conn
+    case fetch_token_str(conn) do
+      {:ok, token} ->
+        with {:ok, user, token_record} <- fetch_user_and_token(token) do
+          conn
+          |> assign(:token, token_record)
+          |> assign(:user, user)
+        else
+          _ ->
+            # token found, but maybe only with app
+            with {:ok, app, token_record} <- fetch_app_and_token(token) do
+              conn
+              |> assign(:token, token_record)
+              |> assign(:app, app)
+            else
+              _ -> conn
+            end
+        end
+
+      _ ->
+        conn
     end
   end
 
@@ -54,6 +76,16 @@ defmodule Pleroma.Plugs.OAuthPlug do
     end
   end
 
+  @spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil
+  defp fetch_app_and_token(token) do
+    query =
+      from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app])
+
+    with %Token{app: app} = token_record <- Repo.one(query) do
+      {:ok, app, token_record}
+    end
+  end
+
   # Gets token from session by :oauth_token key
   #
   @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
diff --git a/lib/pleroma/plugs/rate_limit_plug.ex b/lib/pleroma/plugs/rate_limit_plug.ex
new file mode 100644 (file)
index 0000000..466f64a
--- /dev/null
@@ -0,0 +1,36 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.RateLimitPlug do
+  import Phoenix.Controller, only: [json: 2]
+  import Plug.Conn
+
+  def init(opts), do: opts
+
+  def call(conn, opts) do
+    enabled? = Pleroma.Config.get([:app_account_creation, :enabled])
+
+    case check_rate(conn, Map.put(opts, :enabled, enabled?)) do
+      {:ok, _count} -> conn
+      {:error, _count} -> render_error(conn)
+      %Plug.Conn{} = conn -> conn
+    end
+  end
+
+  defp check_rate(conn, %{enabled: true} = opts) do
+    max_requests = opts[:max_requests]
+    bucket_name = conn.remote_ip |> Tuple.to_list() |> Enum.join(".")
+
+    ExRated.check_rate(bucket_name, opts[:interval] * 1000, max_requests)
+  end
+
+  defp check_rate(conn, _), do: conn
+
+  defp render_error(conn) do
+    conn
+    |> put_status(:forbidden)
+    |> json(%{error: "Rate limit exceeded."})
+    |> halt()
+  end
+end
index 427400aa17764bc548233af00531af39786c1d7c..474de9ba5c7ec716c99fd0ed4ec55c878dd36fba 100644 (file)
@@ -204,14 +204,15 @@ defmodule Pleroma.User do
   end
 
   def register_changeset(struct, params \\ %{}, opts \\ []) do
-    confirmation_status =
-      if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
-        :confirmed
+    need_confirmation? =
+      if is_nil(opts[:need_confirmation]) do
+        Pleroma.Config.get([:instance, :account_activation_required])
       else
-        :unconfirmed
+        opts[:need_confirmation]
       end
 
-    info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)
+    info_change =
+      User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
 
     changeset =
       struct
index 1b81619cef86cc377f11464b1fededd2ff8e71d8..5a50ee639a383aa7e499214d22e33158f61373bd 100644 (file)
@@ -8,6 +8,8 @@ defmodule Pleroma.User.Info do
 
   alias Pleroma.User.Info
 
+  @type t :: %__MODULE__{}
+
   embedded_schema do
     field(:banner, :map, default: %{})
     field(:background, :map, default: %{})
@@ -210,21 +212,23 @@ defmodule Pleroma.User.Info do
     ])
   end
 
-  def confirmation_changeset(info, :confirmed) do
-    confirmation_changeset(info, %{
-      confirmation_pending: false,
-      confirmation_token: nil
-    })
-  end
+  @spec confirmation_changeset(Info.t(), keyword()) :: Ecto.Changerset.t()
+  def confirmation_changeset(info, opts) do
+    need_confirmation? = Keyword.get(opts, :need_confirmation)
 
-  def confirmation_changeset(info, :unconfirmed) do
-    confirmation_changeset(info, %{
-      confirmation_pending: true,
-      confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
-    })
-  end
+    params =
+      if need_confirmation? do
+        %{
+          confirmation_pending: true,
+          confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
+        }
+      else
+        %{
+          confirmation_pending: false,
+          confirmation_token: nil
+        }
+      end
 
-  def confirmation_changeset(info, params) do
     cast(info, params, [:confirmation_pending, :confirmation_token])
   end
 
index b553d96a8e056666ccc2f97655fbc260da6f9642..e00b33aba698585f43adc1254096231ac6180afe 100644 (file)
@@ -59,7 +59,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
       bio: "."
     }
 
-    changeset = User.register_changeset(%User{}, user_data, confirmed: true)
+    changeset = User.register_changeset(%User{}, user_data, need_confirmation: false)
     {:ok, user} = User.register(changeset)
 
     conn
index dd79cdcf7f8c993b98e2db42e7d372c05acf949e..c4a6fce08113b76932c04df6fc116d44f3a747ad 100644 (file)
@@ -74,7 +74,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
                password_confirmation: random_password
              },
              external: true,
-             confirmed: true
+             need_confirmation: false
            )
            |> Repo.insert(),
          {:ok, _} <-
index fd595031daf57905cd3250aec46bf7a1ac917604..defd88a44dad4363cf876b85625c73f53036518f 100644 (file)
@@ -39,12 +39,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   alias Pleroma.Web.OAuth.Authorization
   alias Pleroma.Web.OAuth.Scopes
   alias Pleroma.Web.OAuth.Token
+  alias Pleroma.Web.TwitterAPI.TwitterAPI
 
   alias Pleroma.Web.ControllerHelper
   import Ecto.Query
 
   require Logger
 
+  plug(
+    Pleroma.Plugs.RateLimitPlug,
+    %{
+      max_requests: Config.get([:app_account_creation, :max_requests]),
+      interval: Config.get([:app_account_creation, :interval])
+    }
+    when action in [:account_register]
+  )
+
   @httpoison Application.get_env(:pleroma, :httpoison)
   @local_mastodon_name "Mastodon-Local"
 
@@ -1693,6 +1703,53 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
+  def account_register(
+        %{assigns: %{app: app}} = conn,
+        %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
+      ) do
+    params =
+      params
+      |> Map.take([
+        "email",
+        "captcha_solution",
+        "captcha_token",
+        "captcha_answer_data",
+        "token",
+        "password"
+      ])
+      |> Map.put("nickname", nickname)
+      |> Map.put("fullname", params["fullname"] || nickname)
+      |> Map.put("bio", params["bio"] || "")
+      |> Map.put("confirm", params["password"])
+
+    with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
+         {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
+      json(conn, %{
+        token_type: "Bearer",
+        access_token: token.token,
+        scope: app.scopes,
+        created_at: Token.Utils.format_created_at(token)
+      })
+    else
+      {:error, errors} ->
+        conn
+        |> put_status(400)
+        |> json(Jason.encode!(errors))
+    end
+  end
+
+  def account_register(%{assigns: %{app: _app}} = conn, _params) do
+    conn
+    |> put_status(400)
+    |> json(%{error: "Missing parameters"})
+  end
+
+  def account_register(conn, _) do
+    conn
+    |> put_status(403)
+    |> json(%{error: "Invalid credentials"})
+  end
+
   def conversations(%{assigns: %{user: user}} = conn, params) do
     participations = Participation.for_user_with_last_activity_id(user, params)
 
index bccc2ac967f48c4394c9f3bc47e9bc48daafc8f2..ddcdb18718b39c7486b3dee5a871145bcb965001 100644 (file)
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.OAuth.App do
   import Ecto.Changeset
 
   @type t :: %__MODULE__{}
+
   schema "apps" do
     field(:client_name, :string)
     field(:redirect_uris, :string)
index ca3901cc4af3f6c3075da29349e87616dc8b35cc..b47688de1d276524daf11820f086b9ef35980a59 100644 (file)
@@ -14,6 +14,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
   import Ecto.Query
 
   @type t :: %__MODULE__{}
+
   schema "oauth_authorizations" do
     field(:token, :string)
     field(:scopes, {:array, :string}, default: [])
@@ -25,28 +26,45 @@ defmodule Pleroma.Web.OAuth.Authorization do
     timestamps()
   end
 
+  @spec create_authorization(App.t(), User.t() | %{}, [String.t()] | nil) ::
+          {:ok, Authorization.t()} | {:error, Changeset.t()}
   def create_authorization(%App{} = app, %User{} = user, scopes \\ nil) do
-    scopes = scopes || app.scopes
-    token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
-
-    authorization = %Authorization{
-      token: token,
-      used: false,
+    %{
+      scopes: scopes || app.scopes,
       user_id: user.id,
-      app_id: app.id,
-      scopes: scopes,
-      valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
+      app_id: app.id
     }
+    |> create_changeset()
+    |> Repo.insert()
+  end
+
+  @spec create_changeset(map()) :: Changeset.t()
+  def create_changeset(attrs \\ %{}) do
+    %Authorization{}
+    |> cast(attrs, [:user_id, :app_id, :scopes, :valid_until])
+    |> validate_required([:app_id, :scopes])
+    |> add_token()
+    |> add_lifetime()
+  end
+
+  defp add_token(changeset) do
+    token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
+    put_change(changeset, :token, token)
+  end
 
-    Repo.insert(authorization)
+  defp add_lifetime(changeset) do
+    put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10))
   end
 
+  @spec use_changeset(Authtorizatiton.t(), map()) :: Changeset.t()
   def use_changeset(%Authorization{} = auth, params) do
     auth
     |> cast(params, [:used])
     |> validate_required([:used])
   end
 
+  @spec use_token(Authorization.t()) ::
+          {:ok, Authorization.t()} | {:error, Changeset.t()} | {:error, String.t()}
   def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do
     if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do
       Repo.update(use_changeset(auth, %{used: true}))
@@ -57,6 +75,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
 
   def use_token(%Authorization{used: true}), do: {:error, "already used"}
 
+  @spec delete_user_authorizations(User.t()) :: {integer(), any()}
   def delete_user_authorizations(%User{id: user_id}) do
     from(
       a in Pleroma.Web.OAuth.Authorization,
index 8ee0da6676755a66c345304a0cd577b38a1d66de..862b8f8c92d00c4110033d9ae5eabfeea19f6616 100644 (file)
@@ -218,6 +218,28 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     token_exchange(conn, params)
   end
 
+  def token_exchange(conn, %{"grant_type" => "client_credentials"} = params) do
+    with %App{} = app <- get_app_from_request(conn, params),
+         {:ok, auth} <- Authorization.create_authorization(app, %User{}),
+         {:ok, token} <- Token.exchange_token(app, auth),
+         {:ok, inserted_at} <- DateTime.from_naive(token.inserted_at, "Etc/UTC") do
+      response = %{
+        token_type: "Bearer",
+        access_token: token.token,
+        refresh_token: token.refresh_token,
+        created_at: DateTime.to_unix(inserted_at),
+        expires_in: 60 * 10,
+        scope: Enum.join(token.scopes, " ")
+      }
+
+      json(conn, response)
+    else
+      _error ->
+        put_status(conn, 400)
+        |> json(%{error: "Invalid credentials"})
+    end
+  end
+
   # Bad request
   def token_exchange(conn, params), do: bad_request(conn, params)
 
index 4e5d1d1180a28c6c67758389919c4f9bd9fd2c0c..ef047d565558614ffebf25f4852b758f07b39699 100644 (file)
@@ -45,12 +45,16 @@ defmodule Pleroma.Web.OAuth.Token do
     |> Repo.find_resource()
   end
 
+  @spec exchange_token(App.t(), Authorization.t()) ::
+          {:ok, Token.t()} | {:error, Changeset.t()}
   def exchange_token(app, auth) do
     with {:ok, auth} <- Authorization.use_token(auth),
          true <- auth.app_id == app.id do
+      user = if auth.user_id, do: User.get_cached_by_id(auth.user_id), else: %User{}
+
       create_token(
         app,
-        User.get_cached_by_id(auth.user_id),
+        user,
         %{scopes: auth.scopes}
       )
     end
@@ -81,12 +85,13 @@ defmodule Pleroma.Web.OAuth.Token do
     |> validate_required([:valid_until])
   end
 
+  @spec create_token(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()}
   def create_token(%App{} = app, %User{} = user, attrs \\ %{}) do
     %__MODULE__{user_id: user.id, app_id: app.id}
     |> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes])
-    |> validate_required([:scopes, :user_id, :app_id])
+    |> validate_required([:scopes, :app_id])
     |> put_valid_until(attrs)
-    |> put_token
+    |> put_token()
     |> put_refresh_token(attrs)
     |> Repo.insert()
   end
index 8b84fbbad1fcba9d05806dbc4a6fa28a3c84ea8e..51146d010606fd0734a221241ca91cbb0e918c55 100644 (file)
@@ -385,6 +385,8 @@ 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)
index 3a777464784078dda93c10e388a8c41d11463e02..1362ef57cf81894c2251721e98511844a7733eae 100644 (file)
@@ -128,7 +128,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
     end
   end
 
-  def register_user(params) do
+  def register_user(params, opts \\ []) do
     token = params["token"]
 
     params = %{
@@ -162,13 +162,22 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
       # I have no idea how this error handling works
       {:error, %{error: Jason.encode!(%{captcha: [error]})}}
     else
-      registrations_open = Pleroma.Config.get([:instance, :registrations_open])
-      registration_process(registrations_open, params, token)
+      registration_process(
+        params,
+        %{
+          registrations_open: Pleroma.Config.get([:instance, :registrations_open]),
+          token: token
+        },
+        opts
+      )
     end
   end
 
-  defp registration_process(registration_open, params, token)
-       when registration_open == false or is_nil(registration_open) do
+  defp registration_process(params, %{registrations_open: true}, opts) do
+    create_user(params, opts)
+  end
+
+  defp registration_process(params, %{token: token}, opts) do
     invite =
       unless is_nil(token) do
         Repo.get_by(UserInviteToken, %{token: token})
@@ -182,19 +191,15 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
 
       invite when valid_invite? ->
         UserInviteToken.update_usage!(invite)
-        create_user(params)
+        create_user(params, opts)
 
       _ ->
         {:error, "Expired token"}
     end
   end
 
-  defp registration_process(true, params, _token) do
-    create_user(params)
-  end
-
-  defp create_user(params) do
-    changeset = User.register_changeset(%User{}, params)
+  defp create_user(params, opts) do
+    changeset = User.register_changeset(%User{}, params, opts)
 
     case User.register(changeset) do
       {:ok, user} ->
index 21e6c555a671883181fb997a285eec1d473ebf09..3c5a70be99308e36df2ef524d21d58aa8c160a8a 100644 (file)
@@ -440,7 +440,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
          true <- user.local,
          true <- user.info.confirmation_pending,
          true <- user.info.confirmation_token == token,
-         info_change <- User.Info.confirmation_changeset(user.info, :confirmed),
+         info_change <- User.Info.confirmation_changeset(user.info, need_confirmation: false),
          changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
          {:ok, _} <- User.update_and_set_cache(changeset) do
       conn
diff --git a/mix.exs b/mix.exs
index 5600aaa4228e65ffaf0f20fc573f83fe10d71cff..b7b9d534df940fbf2acf818ee68dac1f36a720ed 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -114,6 +114,7 @@ defmodule Pleroma.Mixfile do
       {:quack, "~> 0.1.1"},
       {:benchee, "~> 1.0"},
       {:esshd, "~> 0.1.0"},
+      {:ex_rated, "~> 1.2"},
       {:plug_static_index_html, "~> 1.0.0"}
     ] ++ oauth_deps
   end
index 981cc17478fd5addaf151b073ce59ccd71c81026..0b24818c5c97fef285181946c65b5cd5138802a1 100644 (file)
--- a/mix.lock
+++ b/mix.lock
   "ecto_sql": {:hex, :ecto_sql, "3.0.5", "7e44172b4f7aca4469f38d7f6a3da394dbf43a1bcf0ca975e958cb957becd74e", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.6", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
   "esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"},
   "eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"},
+  "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"},
   "ex_aws": {:hex, :ex_aws, "2.1.0", "b92651527d6c09c479f9013caa9c7331f19cba38a650590d82ebf2c6c16a1d8a", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"},
   "ex_aws_s3": {:hex, :ex_aws_s3, "2.0.1", "9e09366e77f25d3d88c5393824e613344631be8db0d1839faca49686e99b6704", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"},
   "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
   "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"},
+  "ex_rated": {:hex, :ex_rated, "1.3.2", "6aeb32abb46ea6076f417a9ce8cb1cf08abf35fb2d42375beaad4dd72b550bf1", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm"},
   "ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]},
   "floki": {:hex, :floki, "0.20.4", "be42ac911fece24b4c72f3b5846774b6e61b83fe685c2fc9d62093277fb3bc86", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
   "gen_smtp": {:hex, :gen_smtp, "0.13.0", "11f08504c4bdd831dc520b8f84a1dce5ce624474a797394e7aafd3c29f5dcd25", [:rebar3], [], "hexpm"},
diff --git a/test/plugs/rate_limit_plug_test.exs b/test/plugs/rate_limit_plug_test.exs
new file mode 100644 (file)
index 0000000..2ec9a8f
--- /dev/null
@@ -0,0 +1,50 @@
+defmodule Pleroma.Plugs.RateLimitPlugTest do
+  use ExUnit.Case, async: true
+  use Plug.Test
+
+  alias Pleroma.Plugs.RateLimitPlug
+
+  @opts RateLimitPlug.init(%{max_requests: 5, interval: 1})
+
+  setup do
+    enabled = Pleroma.Config.get([:app_account_creation, :enabled])
+
+    Pleroma.Config.put([:app_account_creation, :enabled], true)
+
+    on_exit(fn ->
+      Pleroma.Config.put([:app_account_creation, :enabled], enabled)
+    end)
+
+    :ok
+  end
+
+  test "it restricts by opts" do
+    conn = conn(:get, "/")
+    bucket_name = conn.remote_ip |> Tuple.to_list() |> Enum.join(".")
+    ms = 1000
+
+    conn = RateLimitPlug.call(conn, @opts)
+    {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5)
+    conn = RateLimitPlug.call(conn, @opts)
+    {2, 3, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5)
+    conn = RateLimitPlug.call(conn, @opts)
+    {3, 2, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5)
+    conn = RateLimitPlug.call(conn, @opts)
+    {4, 1, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5)
+    conn = RateLimitPlug.call(conn, @opts)
+    {5, 0, to_reset, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5)
+    conn = RateLimitPlug.call(conn, @opts)
+    assert conn.status == 403
+    assert conn.halted
+    assert conn.resp_body == "{\"error\":\"Rate limit exceeded.\"}"
+
+    Process.sleep(to_reset)
+
+    conn = conn(:get, "/")
+    conn = RateLimitPlug.call(conn, @opts)
+    {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5)
+    refute conn.status == 403
+    refute conn.halted
+    refute conn.resp_body
+  end
+end
index adc77a26416ddd67ecf6c92c3725289ee9fec3c7..60de0206e551552594cd59d05f827a9ebc072112 100644 (file)
@@ -349,7 +349,7 @@ defmodule Pleroma.UserTest do
     end
 
     test "it creates confirmed user if :confirmed option is given" do
-      changeset = User.register_changeset(%User{}, @full_user_data, confirmed: true)
+      changeset = User.register_changeset(%User{}, @full_user_data, need_confirmation: false)
       assert changeset.valid?
 
       {:ok, user} = Repo.insert(changeset)
index 537cd98d56300e7d791539d492bfc24d9e66a396..5c79ee633f8e9e8bcdaecf8647fb9213f9d5d5b0 100644 (file)
@@ -16,6 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.MastodonAPI.FilterView
   alias Pleroma.Web.OAuth.App
+  alias Pleroma.Web.OAuth.Token
   alias Pleroma.Web.OStatus
   alias Pleroma.Web.Push
   alias Pleroma.Web.TwitterAPI.TwitterAPI
@@ -3216,4 +3217,129 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
     assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
   end
+
+  describe "create account by app" do
+    setup do
+      enabled = Pleroma.Config.get([:app_account_creation, :enabled])
+      max_requests = Pleroma.Config.get([:app_account_creation, :max_requests])
+      interval = Pleroma.Config.get([:app_account_creation, :interval])
+
+      Pleroma.Config.put([:app_account_creation, :enabled], true)
+      Pleroma.Config.put([:app_account_creation, :max_requests], 5)
+      Pleroma.Config.put([:app_account_creation, :interval], 1)
+
+      on_exit(fn ->
+        Pleroma.Config.put([:app_account_creation, :enabled], enabled)
+        Pleroma.Config.put([:app_account_creation, :max_requests], max_requests)
+        Pleroma.Config.put([:app_account_creation, :interval], interval)
+      end)
+
+      :ok
+    end
+
+    test "Account registration via Application", %{conn: conn} do
+      conn =
+        conn
+        |> post("/api/v1/apps", %{
+          client_name: "client_name",
+          redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
+          scopes: "read, write, follow"
+        })
+
+      %{
+        "client_id" => client_id,
+        "client_secret" => client_secret,
+        "id" => _,
+        "name" => "client_name",
+        "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
+        "vapid_key" => _,
+        "website" => nil
+      } = json_response(conn, 200)
+
+      conn =
+        conn
+        |> post("/oauth/token", %{
+          grant_type: "client_credentials",
+          client_id: client_id,
+          client_secret: client_secret
+        })
+
+      assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
+               json_response(conn, 200)
+
+      assert token
+      token_from_db = Repo.get_by(Token, token: token)
+      assert token_from_db
+      assert refresh
+      assert scope == "read write follow"
+
+      conn =
+        build_conn()
+        |> put_req_header("authorization", "Bearer " <> token)
+        |> post("/api/v1/accounts", %{
+          username: "lain",
+          email: "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.info.confirmation_pending
+    end
+
+    test "rate limit", %{conn: conn} do
+      app_token = insert(:oauth_token, user: nil)
+
+      conn =
+        put_req_header(conn, "authorization", "Bearer " <> app_token.token)
+        |> Map.put(:remote_ip, {15, 15, 15, 15})
+
+      for i <- 1..5 do
+        conn =
+          conn
+          |> post("/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.info.confirmation_pending
+      end
+
+      conn =
+        conn
+        |> post("/api/v1/accounts", %{
+          username: "6lain",
+          email: "6lain@example.org",
+          password: "PlzDontHackLain",
+          agreement: true
+        })
+
+      assert json_response(conn, 403) == %{"error" => "Rate limit exceeded."}
+    end
+  end
 end
index cb68369838979a66504054b78683d596b94a2e75..1c04ac9ad7c3a8fa051d5c101ae612060891a44a 100644 (file)
@@ -614,6 +614,27 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
       assert token.scopes == ["scope1", "scope2"]
     end
 
+    test "issue a token for client_credentials grant type" do
+      app = insert(:oauth_app, scopes: ["read", "write"])
+
+      conn =
+        build_conn()
+        |> post("/oauth/token", %{
+          "grant_type" => "client_credentials",
+          "client_id" => app.client_id,
+          "client_secret" => app.client_secret
+        })
+
+      assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
+               json_response(conn, 200)
+
+      assert token
+      token_from_db = Repo.get_by(Token, token: token)
+      assert token_from_db
+      assert refresh
+      assert scope == "read write"
+    end
+
     test "rejects token exchange with invalid client credentials" do
       user = insert(:user)
       app = insert(:oauth_app)
@@ -644,7 +665,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
 
       password = "testpassword"
       user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
-      info_change = Pleroma.User.Info.confirmation_changeset(user.info, :unconfirmed)
+      info_change = Pleroma.User.Info.confirmation_changeset(user.info, need_confirmation: true)
 
       {:ok, user} =
         user
index 90718cfb4351c5f342f2bec19bc3c874c314635e..e194f14fb55a80a62493582701cd664eae7f1554 100644 (file)
@@ -1094,7 +1094,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
   describe "GET /api/account/confirm_email/:id/:token" do
     setup do
       user = insert(:user)
-      info_change = User.Info.confirmation_changeset(user.info, :unconfirmed)
+      info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true)
 
       {:ok, user} =
         user
@@ -1145,7 +1145,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
       end
 
       user = insert(:user)
-      info_change = User.Info.confirmation_changeset(user.info, :unconfirmed)
+      info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true)
 
       {:ok, user} =
         user
index d529fd2c358485edca29fceaed3a18510e1a0010..3857d585faac5fd16be48820b9f3f5c7b8228d79 100644 (file)
@@ -4,6 +4,7 @@
 
 defmodule Pleroma.Web.ErrorViewTest do
   use Pleroma.Web.ConnCase, async: true
+  import ExUnit.CaptureLog
 
   # Bring render/3 and render_to_string/3 for testing custom views
   import Phoenix.View
@@ -13,17 +14,23 @@ defmodule Pleroma.Web.ErrorViewTest do
   end
 
   test "render 500.json" do
-    assert render(Pleroma.Web.ErrorView, "500.json", []) ==
-             %{errors: %{detail: "Internal server error", reason: "nil"}}
+    assert capture_log(fn ->
+             assert render(Pleroma.Web.ErrorView, "500.json", []) ==
+                      %{errors: %{detail: "Internal server error", reason: "nil"}}
+           end) =~ "[error] Internal server error: nil"
   end
 
   test "render any other" do
-    assert render(Pleroma.Web.ErrorView, "505.json", []) ==
-             %{errors: %{detail: "Internal server error", reason: "nil"}}
+    assert capture_log(fn ->
+             assert render(Pleroma.Web.ErrorView, "505.json", []) ==
+                      %{errors: %{detail: "Internal server error", reason: "nil"}}
+           end) =~ "[error] Internal server error: nil"
   end
 
   test "render 500.json with reason" do
-    assert render(Pleroma.Web.ErrorView, "500.json", reason: "test reason") ==
-             %{errors: %{detail: "Internal server error", reason: "\"test reason\""}}
+    assert capture_log(fn ->
+             assert render(Pleroma.Web.ErrorView, "500.json", reason: "test reason") ==
+                      %{errors: %{detail: "Internal server error", reason: "\"test reason\""}}
+           end) =~ "[error] Internal server error: \"test reason\""
   end
 end