Merge branch 'features/mastoapi/2.7.0-registration' into 'develop'

Features/mastoapi/2.7.0 registration

Closes #773

See merge request pleroma/pleroma!1134
This commit is contained in:
kaniini 2019-05-13 18:35:45 +00:00
commit f3e8f5b1f2
27 changed files with 477 additions and 67 deletions

View 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/notifications/destroy_multiple` (glitch-soc extension)
- Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension) - Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension)
- Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/) - Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/)
- Mastodon API: REST API for creating an account
- ActivityPub C2S: OAuth endpoints - ActivityPub C2S: OAuth endpoints
- Metadata RelMe provider - Metadata RelMe provider
- OAuth: added support for refresh tokens - 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: Add `with_muted` parameter to timeline endpoints
- Mastodon API: Actual reblog hiding instead of a dummy - Mastodon API: Actual reblog hiding instead of a dummy
- Mastodon API: Remove attachment limit in the Status entity - 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 Cowboy to 2.6
- Deps: Updated Ecto to 3.0.7 - Deps: Updated Ecto to 3.0.7
- Don't ship finmoji by default, they can be installed as an emoji pack - 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` - Admin API: Move the user related API to `api/pleroma/admin/users`
### Fixed ### Fixed

View File

@ -234,6 +234,8 @@
safe_dm_mentions: false, safe_dm_mentions: false,
healthcheck: false healthcheck: false
config :pleroma, :app_account_creation, enabled: false, max_requests: 5, interval: 1800
config :pleroma, :markup, config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because # XXX - unfortunately, inline images must be enabled by default right now, because
# of custom emoji. Issue #275 discusses defanging that somehow. # of custom emoji. Issue #275 discusses defanging that somehow.

View File

@ -87,3 +87,13 @@ Additional parameters can be added to the JSON body/Form data:
`POST /oauth/token` `POST /oauth/token`
Post here request with grant_type=refresh_token to obtain new access token. Returns an access 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.

View 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`) * `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``. * `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 ## :logger
* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack * `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack

View File

@ -138,7 +138,7 @@ def run(["new", nickname, email | rest]) do
bio: bio bio: bio
} }
changeset = User.register_changeset(%User{}, params, confirmed: true) changeset = User.register_changeset(%User{}, params, need_confirmation: false)
{:ok, _user} = User.register(changeset) {:ok, _user} = User.register(changeset)
Mix.shell().info("User #{nickname} created") Mix.shell().info("User #{nickname} created")

View File

@ -8,6 +8,7 @@ defmodule Pleroma.Plugs.OAuthPlug do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
@realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i")
@ -22,18 +23,39 @@ def call(%{params: %{"access_token" => access_token}} = conn, _) do
|> assign(:token, token_record) |> assign(:token, token_record)
|> assign(:user, user) |> assign(:user, user)
else 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
end end
def call(conn, _) do def call(conn, _) do
with {:ok, token_str} <- fetch_token_str(conn), case fetch_token_str(conn) do
{:ok, user, token_record} <- fetch_user_and_token(token_str) do {:ok, token} ->
conn with {:ok, user, token_record} <- fetch_user_and_token(token) do
|> assign(:token, token_record) conn
|> assign(:user, user) |> assign(:token, token_record)
else |> assign(:user, user)
_ -> conn 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
end end
@ -54,6 +76,16 @@ defp fetch_user_and_token(token) do
end end
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 # Gets token from session by :oauth_token key
# #
@spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}

View File

@ -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

View File

@ -204,14 +204,15 @@ def reset_password(user, data) do
end end
def register_changeset(struct, params \\ %{}, opts \\ []) do def register_changeset(struct, params \\ %{}, opts \\ []) do
confirmation_status = need_confirmation? =
if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do if is_nil(opts[:need_confirmation]) do
:confirmed Pleroma.Config.get([:instance, :account_activation_required])
else else
:unconfirmed opts[:need_confirmation]
end end
info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status) info_change =
User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
changeset = changeset =
struct struct

View File

@ -8,6 +8,8 @@ defmodule Pleroma.User.Info do
alias Pleroma.User.Info alias Pleroma.User.Info
@type t :: %__MODULE__{}
embedded_schema do embedded_schema do
field(:banner, :map, default: %{}) field(:banner, :map, default: %{})
field(:background, :map, default: %{}) field(:background, :map, default: %{})
@ -210,21 +212,23 @@ def profile_update(info, params) do
]) ])
end end
def confirmation_changeset(info, :confirmed) do @spec confirmation_changeset(Info.t(), keyword()) :: Ecto.Changerset.t()
confirmation_changeset(info, %{ def confirmation_changeset(info, opts) do
confirmation_pending: false, need_confirmation? = Keyword.get(opts, :need_confirmation)
confirmation_token: nil
})
end
def confirmation_changeset(info, :unconfirmed) do params =
confirmation_changeset(info, %{ if need_confirmation? do
confirmation_pending: true, %{
confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64() confirmation_pending: true,
}) confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
end }
else
%{
confirmation_pending: false,
confirmation_token: nil
}
end
def confirmation_changeset(info, params) do
cast(info, params, [:confirmation_pending, :confirmation_token]) cast(info, params, [:confirmation_pending, :confirmation_token])
end end

View File

@ -59,7 +59,7 @@ def user_create(
bio: "." 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) {:ok, user} = User.register(changeset)
conn conn

View File

@ -74,7 +74,7 @@ def create_from_registration(
password_confirmation: random_password password_confirmation: random_password
}, },
external: true, external: true,
confirmed: true need_confirmation: false
) )
|> Repo.insert(), |> Repo.insert(),
{:ok, _} <- {:ok, _} <-

View File

@ -39,12 +39,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Scopes alias Pleroma.Web.OAuth.Scopes
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.ControllerHelper alias Pleroma.Web.ControllerHelper
import Ecto.Query import Ecto.Query
require Logger 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) @httpoison Application.get_env(:pleroma, :httpoison)
@local_mastodon_name "Mastodon-Local" @local_mastodon_name "Mastodon-Local"
@ -1693,6 +1703,53 @@ def reports(%{assigns: %{user: user}} = conn, params) do
end end
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 def conversations(%{assigns: %{user: user}} = conn, params) do
participations = Participation.for_user_with_last_activity_id(user, params) participations = Participation.for_user_with_last_activity_id(user, params)

View File

@ -7,6 +7,7 @@ defmodule Pleroma.Web.OAuth.App do
import Ecto.Changeset import Ecto.Changeset
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
schema "apps" do schema "apps" do
field(:client_name, :string) field(:client_name, :string)
field(:redirect_uris, :string) field(:redirect_uris, :string)

View File

@ -14,6 +14,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
import Ecto.Query import Ecto.Query
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
schema "oauth_authorizations" do schema "oauth_authorizations" do
field(:token, :string) field(:token, :string)
field(:scopes, {:array, :string}, default: []) field(:scopes, {:array, :string}, default: [])
@ -25,28 +26,45 @@ defmodule Pleroma.Web.OAuth.Authorization do
timestamps() timestamps()
end 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 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) scopes: scopes || app.scopes,
authorization = %Authorization{
token: token,
used: false,
user_id: user.id, user_id: user.id,
app_id: app.id, app_id: app.id
scopes: scopes,
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
} }
|> create_changeset()
Repo.insert(authorization) |> Repo.insert()
end 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 def use_changeset(%Authorization{} = auth, params) do
auth auth
|> cast(params, [:used]) |> cast(params, [:used])
|> validate_required([:used]) |> validate_required([:used])
end 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 def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do
if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do
Repo.update(use_changeset(auth, %{used: true})) Repo.update(use_changeset(auth, %{used: true}))
@ -57,6 +75,7 @@ def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do
def use_token(%Authorization{used: true}), do: {:error, "already used"} 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 def delete_user_authorizations(%User{id: user_id}) do
from( from(
a in Pleroma.Web.OAuth.Authorization, a in Pleroma.Web.OAuth.Authorization,

View File

@ -218,6 +218,28 @@ def token_exchange(
token_exchange(conn, params) token_exchange(conn, params)
end 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 # Bad request
def token_exchange(conn, params), do: bad_request(conn, params) def token_exchange(conn, params), do: bad_request(conn, params)

View File

@ -45,12 +45,16 @@ def get_by_refresh_token(%App{id: app_id} = _app, token) do
|> Repo.find_resource() |> Repo.find_resource()
end end
@spec exchange_token(App.t(), Authorization.t()) ::
{:ok, Token.t()} | {:error, Changeset.t()}
def exchange_token(app, auth) do def exchange_token(app, auth) do
with {:ok, auth} <- Authorization.use_token(auth), with {:ok, auth} <- Authorization.use_token(auth),
true <- auth.app_id == app.id do 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( create_token(
app, app,
User.get_cached_by_id(auth.user_id), user,
%{scopes: auth.scopes} %{scopes: auth.scopes}
) )
end end
@ -81,12 +85,13 @@ defp put_valid_until(changeset, attrs) do
|> validate_required([:valid_until]) |> validate_required([:valid_until])
end end
@spec create_token(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()}
def create_token(%App{} = app, %User{} = user, attrs \\ %{}) do def create_token(%App{} = app, %User{} = user, attrs \\ %{}) do
%__MODULE__{user_id: user.id, app_id: app.id} %__MODULE__{user_id: user.id, app_id: app.id}
|> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes]) |> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes])
|> validate_required([:scopes, :user_id, :app_id]) |> validate_required([:scopes, :app_id])
|> put_valid_until(attrs) |> put_valid_until(attrs)
|> put_token |> put_token()
|> put_refresh_token(attrs) |> put_refresh_token(attrs)
|> Repo.insert() |> Repo.insert()
end end

View File

@ -385,6 +385,8 @@ defmodule Pleroma.Web.Router do
scope "/api/v1", Pleroma.Web.MastodonAPI do scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api) pipe_through(:api)
post("/accounts", MastodonAPIController, :account_register)
get("/instance", MastodonAPIController, :masto_instance) get("/instance", MastodonAPIController, :masto_instance)
get("/instance/peers", MastodonAPIController, :peers) get("/instance/peers", MastodonAPIController, :peers)
post("/apps", MastodonAPIController, :create_app) post("/apps", MastodonAPIController, :create_app)

View File

@ -128,7 +128,7 @@ def upload(%Plug.Upload{} = file, %User{} = user, format \\ "xml") do
end end
end end
def register_user(params) do def register_user(params, opts \\ []) do
token = params["token"] token = params["token"]
params = %{ params = %{
@ -162,13 +162,22 @@ def register_user(params) do
# I have no idea how this error handling works # I have no idea how this error handling works
{:error, %{error: Jason.encode!(%{captcha: [error]})}} {:error, %{error: Jason.encode!(%{captcha: [error]})}}
else else
registrations_open = Pleroma.Config.get([:instance, :registrations_open]) registration_process(
registration_process(registrations_open, params, token) params,
%{
registrations_open: Pleroma.Config.get([:instance, :registrations_open]),
token: token
},
opts
)
end end
end end
defp registration_process(registration_open, params, token) defp registration_process(params, %{registrations_open: true}, opts) do
when registration_open == false or is_nil(registration_open) do create_user(params, opts)
end
defp registration_process(params, %{token: token}, opts) do
invite = invite =
unless is_nil(token) do unless is_nil(token) do
Repo.get_by(UserInviteToken, %{token: token}) Repo.get_by(UserInviteToken, %{token: token})
@ -182,19 +191,15 @@ defp registration_process(registration_open, params, token)
invite when valid_invite? -> invite when valid_invite? ->
UserInviteToken.update_usage!(invite) UserInviteToken.update_usage!(invite)
create_user(params) create_user(params, opts)
_ -> _ ->
{:error, "Expired token"} {:error, "Expired token"}
end end
end end
defp registration_process(true, params, _token) do defp create_user(params, opts) do
create_user(params) changeset = User.register_changeset(%User{}, params, opts)
end
defp create_user(params) do
changeset = User.register_changeset(%User{}, params)
case User.register(changeset) do case User.register(changeset) do
{:ok, user} -> {:ok, user} ->

View File

@ -440,7 +440,7 @@ def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
true <- user.local, true <- user.local,
true <- user.info.confirmation_pending, true <- user.info.confirmation_pending,
true <- user.info.confirmation_token == token, 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), changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
{:ok, _} <- User.update_and_set_cache(changeset) do {:ok, _} <- User.update_and_set_cache(changeset) do
conn conn

View File

@ -114,6 +114,7 @@ defp deps do
{:quack, "~> 0.1.1"}, {:quack, "~> 0.1.1"},
{:benchee, "~> 1.0"}, {:benchee, "~> 1.0"},
{:esshd, "~> 0.1.0"}, {:esshd, "~> 0.1.0"},
{:ex_rated, "~> 1.2"},
{:plug_static_index_html, "~> 1.0.0"} {:plug_static_index_html, "~> 1.0.0"}
] ++ oauth_deps ] ++ oauth_deps
end end

View File

@ -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"}, "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"}, "esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"},
"eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [: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": {: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_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_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_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"]}, "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"}, "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"}, "gen_smtp": {:hex, :gen_smtp, "0.13.0", "11f08504c4bdd831dc520b8f84a1dce5ce624474a797394e7aafd3c29f5dcd25", [:rebar3], [], "hexpm"},

View File

@ -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

View File

@ -349,7 +349,7 @@ test "it creates unconfirmed user" do
end end
test "it creates confirmed user if :confirmed option is given" do 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? assert changeset.valid?
{:ok, user} = Repo.insert(changeset) {:ok, user} = Repo.insert(changeset)

View File

@ -16,6 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.FilterView alias Pleroma.Web.MastodonAPI.FilterView
alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OStatus alias Pleroma.Web.OStatus
alias Pleroma.Web.Push alias Pleroma.Web.Push
alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.TwitterAPI.TwitterAPI
@ -3216,4 +3217,129 @@ test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{c
replied_to_user = User.get_by_ap_id(replied_to.data["actor"]) 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 assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
end 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 end

View File

@ -614,6 +614,27 @@ test "issues a token for request with HTTP basic auth client credentials" do
assert token.scopes == ["scope1", "scope2"] assert token.scopes == ["scope1", "scope2"]
end 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 test "rejects token exchange with invalid client credentials" do
user = insert(:user) user = insert(:user)
app = insert(:oauth_app) app = insert(:oauth_app)
@ -644,7 +665,7 @@ test "rejects token exchange for valid credentials belonging to unconfirmed user
password = "testpassword" password = "testpassword"
user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password)) 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} = {:ok, user} =
user user

View File

@ -1094,7 +1094,7 @@ test "it returns 500 when user is not local", %{conn: conn, user: user} do
describe "GET /api/account/confirm_email/:id/:token" do describe "GET /api/account/confirm_email/:id/:token" do
setup do setup do
user = insert(:user) 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} = {:ok, user} =
user user
@ -1145,7 +1145,7 @@ test "it returns 500 if token is invalid", %{conn: conn, user: user} do
end end
user = insert(:user) 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} = {:ok, user} =
user user

View File

@ -4,6 +4,7 @@
defmodule Pleroma.Web.ErrorViewTest do defmodule Pleroma.Web.ErrorViewTest do
use Pleroma.Web.ConnCase, async: true use Pleroma.Web.ConnCase, async: true
import ExUnit.CaptureLog
# Bring render/3 and render_to_string/3 for testing custom views # Bring render/3 and render_to_string/3 for testing custom views
import Phoenix.View import Phoenix.View
@ -13,17 +14,23 @@ test "renders 404.json" do
end end
test "render 500.json" do test "render 500.json" do
assert render(Pleroma.Web.ErrorView, "500.json", []) == assert capture_log(fn ->
%{errors: %{detail: "Internal server error", reason: "nil"}} assert render(Pleroma.Web.ErrorView, "500.json", []) ==
%{errors: %{detail: "Internal server error", reason: "nil"}}
end) =~ "[error] Internal server error: nil"
end end
test "render any other" do test "render any other" do
assert render(Pleroma.Web.ErrorView, "505.json", []) == assert capture_log(fn ->
%{errors: %{detail: "Internal server error", reason: "nil"}} assert render(Pleroma.Web.ErrorView, "505.json", []) ==
%{errors: %{detail: "Internal server error", reason: "nil"}}
end) =~ "[error] Internal server error: nil"
end end
test "render 500.json with reason" do test "render 500.json with reason" do
assert render(Pleroma.Web.ErrorView, "500.json", reason: "test reason") == assert capture_log(fn ->
%{errors: %{detail: "Internal server error", reason: "\"test reason\""}} 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
end end