From a2be420f940fb8f181feeb9b0fb9759d433dcae1 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 13 May 2019 18:35:45 +0000 Subject: [PATCH] differences_in_mastoapi_responses.md: fullname & bio are optionnal [ci skip] --- CHANGELOG.md | 3 +- config/config.exs | 2 + docs/api/differences_in_mastoapi_responses.md | 10 ++ docs/config.md | 6 + lib/mix/tasks/pleroma/user.ex | 2 +- lib/pleroma/plugs/oauth_plug.ex | 48 +++++-- lib/pleroma/plugs/rate_limit_plug.ex | 36 +++++ lib/pleroma/user.ex | 11 +- lib/pleroma/user/info.ex | 30 +++-- .../web/admin_api/admin_api_controller.ex | 2 +- lib/pleroma/web/auth/pleroma_authenticator.ex | 2 +- .../mastodon_api/mastodon_api_controller.ex | 57 ++++++++ lib/pleroma/web/oauth/app.ex | 1 + lib/pleroma/web/oauth/authorization.ex | 41 ++++-- lib/pleroma/web/oauth/oauth_controller.ex | 22 +++ lib/pleroma/web/oauth/token.ex | 11 +- lib/pleroma/web/router.ex | 2 + lib/pleroma/web/twitter_api/twitter_api.ex | 29 ++-- .../web/twitter_api/twitter_api_controller.ex | 2 +- mix.exs | 1 + mix.lock | 2 + test/plugs/rate_limit_plug_test.exs | 50 +++++++ test/user_test.exs | 2 +- .../mastodon_api_controller_test.exs | 126 ++++++++++++++++++ test/web/oauth/oauth_controller_test.exs | 23 +++- .../twitter_api_controller_test.exs | 4 +- test/web/views/error_view_test.exs | 19 ++- 27 files changed, 477 insertions(+), 67 deletions(-) create mode 100644 lib/pleroma/plugs/rate_limit_plug.ex create mode 100644 test/plugs/rate_limit_plug_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e6f6b39..cb934dab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/config/config.exs b/config/config.exs index 1e64b79a7..e8aad855c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -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. diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md index d3ba41b6a..36b47608e 100644 --- a/docs/api/differences_in_mastoapi_responses.md +++ b/docs/api/differences_in_mastoapi_responses.md @@ -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. diff --git a/docs/config.md b/docs/config.md index 43ea24d80..470f71b7c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -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 diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 6a83a8c0d..d130ff8c9 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -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") diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex index 9d43732eb..86bc4aa3a 100644 --- a/lib/pleroma/plugs/oauth_plug.ex +++ b/lib/pleroma/plugs/oauth_plug.ex @@ -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 index 000000000..466f64a79 --- /dev/null +++ b/lib/pleroma/plugs/rate_limit_plug.ex @@ -0,0 +1,36 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 427400aa1..474de9ba5 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -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 diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 1b81619ce..5a50ee639 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -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 diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index b553d96a8..e00b33aba 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -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 diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index dd79cdcf7..c4a6fce08 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -74,7 +74,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do password_confirmation: random_password }, external: true, - confirmed: true + need_confirmation: false ) |> Repo.insert(), {:ok, _} <- diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index fd595031d..defd88a44 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -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) diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex index bccc2ac96..ddcdb1871 100644 --- a/lib/pleroma/web/oauth/app.ex +++ b/lib/pleroma/web/oauth/app.ex @@ -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) diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex index ca3901cc4..b47688de1 100644 --- a/lib/pleroma/web/oauth/authorization.ex +++ b/lib/pleroma/web/oauth/authorization.ex @@ -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 } - - Repo.insert(authorization) + |> 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 + + 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, diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 8ee0da667..862b8f8c9 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -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) diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index 4e5d1d118..ef047d565 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -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 diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 8b84fbbad..51146d010 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -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) diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 3a7774647..1362ef57c 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -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} -> diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 21e6c555a..3c5a70be9 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -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 5600aaa42..b7b9d534d 100644 --- 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 diff --git a/mix.lock b/mix.lock index 981cc1747..0b24818c5 100644 --- a/mix.lock +++ b/mix.lock @@ -24,10 +24,12 @@ "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 index 000000000..2ec9a8fb7 --- /dev/null +++ b/test/plugs/rate_limit_plug_test.exs @@ -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 diff --git a/test/user_test.exs b/test/user_test.exs index adc77a264..60de0206e 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -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) diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 537cd98d5..5c79ee633 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -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 diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index cb6836983..1c04ac9ad 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -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 diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs index 90718cfb4..e194f14fb 100644 --- a/test/web/twitter_api/twitter_api_controller_test.exs +++ b/test/web/twitter_api/twitter_api_controller_test.exs @@ -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 diff --git a/test/web/views/error_view_test.exs b/test/web/views/error_view_test.exs index d529fd2c3..3857d585f 100644 --- a/test/web/views/error_view_test.exs +++ b/test/web/views/error_view_test.exs @@ -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