Add rate-limiting on queries with Hammer
Closes #67 Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -44,6 +44,9 @@ defmodule Mobilizon.GraphQL.Authorization do
|
||||
def get_user_role(%{role: role}), do: role
|
||||
def get_user_role(nil), do: nil
|
||||
|
||||
@impl true
|
||||
def get_ip(%{ip: ip}), do: ip
|
||||
|
||||
@impl true
|
||||
def unauthorized_message(resolution) do
|
||||
case Map.get(resolution.context, :current_user) do
|
||||
|
||||
@@ -15,6 +15,9 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||
import_types(Schema.Events.ParticipantType)
|
||||
import_types(Schema.TagType)
|
||||
|
||||
@env Application.compile_env(:mobilizon, :env)
|
||||
@event_rate_limiting 60
|
||||
|
||||
@desc "An event"
|
||||
object :event do
|
||||
meta(:authorize, :all)
|
||||
@@ -437,6 +440,8 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||
args: %{organizer_actor_id: :organizer_actor_id}
|
||||
)
|
||||
|
||||
middleware(Rajska.RateLimiter, limit: event_rate_limiting(@env))
|
||||
|
||||
resolve(&Event.create_event/3)
|
||||
end
|
||||
|
||||
@@ -505,4 +510,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||
resolve(&Event.delete_event/3)
|
||||
end
|
||||
end
|
||||
|
||||
defp event_rate_limiting(:test), do: @event_rate_limiting * 1000
|
||||
defp event_rate_limiting(_), do: @event_rate_limiting
|
||||
end
|
||||
|
||||
@@ -6,6 +6,9 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
|
||||
|
||||
alias Mobilizon.GraphQL.Resolvers.Media
|
||||
|
||||
@env Application.compile_env(:mobilizon, :env)
|
||||
@media_rate_limiting 60
|
||||
|
||||
@desc "A media"
|
||||
object :media do
|
||||
meta(:authorize, :all)
|
||||
@@ -77,6 +80,8 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
|
||||
args: %{}
|
||||
)
|
||||
|
||||
middleware(Rajska.RateLimiter, limit: media_rate_limiting(@env))
|
||||
|
||||
resolve(&Media.upload_media/3)
|
||||
end
|
||||
|
||||
@@ -95,4 +100,7 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
|
||||
resolve(&Media.remove_media/3)
|
||||
end
|
||||
end
|
||||
|
||||
defp media_rate_limiting(:test), do: @media_rate_limiting * 1000
|
||||
defp media_rate_limiting(_), do: @media_rate_limiting
|
||||
end
|
||||
|
||||
@@ -7,12 +7,17 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 2]
|
||||
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.GraphQL.Resolvers.{Application, Media, User}
|
||||
alias Mobilizon.GraphQL.Resolvers.Application, as: ApplicationResolver
|
||||
alias Mobilizon.GraphQL.Resolvers.{Media, User}
|
||||
alias Mobilizon.GraphQL.Resolvers.Users.ActivitySettings
|
||||
alias Mobilizon.GraphQL.Schema
|
||||
|
||||
import_types(Schema.SortType)
|
||||
|
||||
@env Application.compile_env(:mobilizon, :env)
|
||||
@user_ip_limit 10
|
||||
@user_email_limit 5
|
||||
|
||||
@desc "A local user of Mobilizon"
|
||||
object :user do
|
||||
meta(:authorize, :all)
|
||||
@@ -180,7 +185,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
description: "The user's authorized authentication apps",
|
||||
meta: [private: true, rule: :forbid_app_access]
|
||||
) do
|
||||
resolve(&Application.get_user_applications/3)
|
||||
resolve(&ApplicationResolver.get_user_applications/3)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -331,6 +336,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
arg(:password, non_null(:string), description: "The new user's password")
|
||||
arg(:locale, :string, description: "The new user's locale")
|
||||
middleware(Rajska.QueryAuthorization, permit: :all)
|
||||
middleware(Rajska.RateLimiter, limit: user_ip_limiter(@env))
|
||||
middleware(Rajska.RateLimiter, keys: :email, limit: user_email_limiter(@env))
|
||||
resolve(&User.create_user/3)
|
||||
end
|
||||
|
||||
@@ -349,6 +356,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
arg(:email, non_null(:string), description: "The email used to register")
|
||||
arg(:locale, :string, description: "The user's locale")
|
||||
middleware(Rajska.QueryAuthorization, permit: :all)
|
||||
middleware(Rajska.RateLimiter, limit: user_ip_limiter(@env))
|
||||
middleware(Rajska.RateLimiter, keys: :email, limit: user_email_limiter(@env))
|
||||
resolve(&User.resend_confirmation_email/3)
|
||||
end
|
||||
|
||||
@@ -357,6 +366,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
arg(:email, non_null(:string), description: "The user's email")
|
||||
arg(:locale, :string, description: "The user's locale")
|
||||
middleware(Rajska.QueryAuthorization, permit: :all)
|
||||
middleware(Rajska.RateLimiter, limit: user_ip_limiter(@env))
|
||||
middleware(Rajska.RateLimiter, keys: :email, limit: user_email_limiter(@env))
|
||||
resolve(&User.send_reset_password/3)
|
||||
end
|
||||
|
||||
@@ -377,6 +388,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
arg(:email, non_null(:string), description: "The user's email")
|
||||
arg(:password, non_null(:string), description: "The user's password")
|
||||
middleware(Rajska.QueryAuthorization, permit: :all)
|
||||
middleware(Rajska.RateLimiter, limit: user_ip_limiter(@env))
|
||||
middleware(Rajska.RateLimiter, keys: :email, limit: user_email_limiter(@env))
|
||||
resolve(&User.login_user/3)
|
||||
end
|
||||
|
||||
@@ -480,4 +493,10 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
resolve(&User.update_locale/3)
|
||||
end
|
||||
end
|
||||
|
||||
defp user_ip_limiter(:test), do: @user_ip_limit * 1000
|
||||
defp user_ip_limiter(_), do: @user_ip_limit
|
||||
|
||||
defp user_email_limiter(:test), do: @user_email_limit * 1000
|
||||
defp user_email_limiter(_), do: @user_email_limit
|
||||
end
|
||||
|
||||
@@ -230,6 +230,9 @@ defmodule Mobilizon.Service.Auth.Applications do
|
||||
%ApplicationDeviceActivation{status: :access_denied} ->
|
||||
{:error, :access_denied}
|
||||
|
||||
%ApplicationDeviceActivation{status: :pending} ->
|
||||
{:error, :pending, @device_code_interval}
|
||||
|
||||
nil ->
|
||||
{:error, :incorrect_device_code}
|
||||
|
||||
|
||||
@@ -15,42 +15,63 @@ defmodule Mobilizon.Web.ApplicationController do
|
||||
conn,
|
||||
%{"name" => name, "redirect_uris" => redirect_uris, "scope" => scope} = args
|
||||
) do
|
||||
case Applications.create(
|
||||
name,
|
||||
String.split(redirect_uris, "\n"),
|
||||
scope,
|
||||
Map.get(args, "website")
|
||||
ip = conn.remote_ip |> :inet.ntoa() |> to_string()
|
||||
|
||||
case Hammer.check_rate(
|
||||
"create_application:#{ip}",
|
||||
60_000,
|
||||
10
|
||||
) do
|
||||
{:ok, %Application{} = app} ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_header("cache-control", "no-store")
|
||||
|> json(
|
||||
Map.take(app, [:name, :website, :redirect_uris, :client_id, :client_secret, :scope])
|
||||
)
|
||||
|
||||
{:error, :invalid_scope} ->
|
||||
conn
|
||||
|> Plug.Conn.put_status(400)
|
||||
|> json(%{
|
||||
"error" => "invalid_scope",
|
||||
"error_description" =>
|
||||
dgettext(
|
||||
"errors",
|
||||
"The scope parameter is not a space separated list of valid scopes"
|
||||
{:allow, _} ->
|
||||
case Applications.create(
|
||||
name,
|
||||
String.split(redirect_uris, "\n"),
|
||||
scope,
|
||||
Map.get(args, "website")
|
||||
) do
|
||||
{:ok, %Application{} = app} ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_header("cache-control", "no-store")
|
||||
|> json(
|
||||
Map.take(app, [:name, :website, :redirect_uris, :client_id, :client_secret, :scope])
|
||||
)
|
||||
})
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error(inspect(error))
|
||||
{:error, :invalid_scope} ->
|
||||
conn
|
||||
|> Plug.Conn.put_status(400)
|
||||
|> json(%{
|
||||
"error" => "invalid_scope",
|
||||
"error_description" =>
|
||||
dgettext(
|
||||
"errors",
|
||||
"The scope parameter is not a space separated list of valid scopes"
|
||||
)
|
||||
})
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error(inspect(error))
|
||||
|
||||
conn
|
||||
|> Plug.Conn.put_status(500)
|
||||
|> json(%{
|
||||
"error" => "server_error",
|
||||
"error_description" =>
|
||||
dgettext(
|
||||
"errors",
|
||||
"Impossible to create application."
|
||||
)
|
||||
})
|
||||
end
|
||||
|
||||
{:deny, _} ->
|
||||
conn
|
||||
|> Plug.Conn.put_status(500)
|
||||
|> Plug.Conn.put_status(429)
|
||||
|> json(%{
|
||||
"error" => "server_error",
|
||||
"error" => "slow_down",
|
||||
"error_description" =>
|
||||
dgettext(
|
||||
"errors",
|
||||
"Impossible to create application."
|
||||
"Too many requests"
|
||||
)
|
||||
})
|
||||
end
|
||||
@@ -148,7 +169,7 @@ defmodule Mobilizon.Web.ApplicationController do
|
||||
"error_description" =>
|
||||
dgettext(
|
||||
"errors",
|
||||
"No application with this client_id was found"
|
||||
"No application was found with this client_id"
|
||||
)
|
||||
})
|
||||
|
||||
@@ -231,6 +252,37 @@ defmodule Mobilizon.Web.ApplicationController do
|
||||
)
|
||||
})
|
||||
|
||||
{:error, :pending, interval} ->
|
||||
case Hammer.check_rate(
|
||||
"generate_device_access_token:#{client_id}:#{device_code}",
|
||||
interval * 1_000,
|
||||
1
|
||||
) do
|
||||
{:allow, _} ->
|
||||
conn
|
||||
|> Plug.Conn.put_status(400)
|
||||
|> json(%{
|
||||
"error" => "authorization_pending",
|
||||
"error_description" =>
|
||||
dgettext(
|
||||
"errors",
|
||||
"The authorization request is still pending"
|
||||
)
|
||||
})
|
||||
|
||||
{:deny, _} ->
|
||||
conn
|
||||
|> Plug.Conn.put_status(400)
|
||||
|> json(%{
|
||||
"error" => "slow_down",
|
||||
"error_description" =>
|
||||
dgettext(
|
||||
"errors",
|
||||
"Please slow down the rate of your requests"
|
||||
)
|
||||
})
|
||||
end
|
||||
|
||||
{:error, :access_denied} ->
|
||||
conn
|
||||
|> Plug.Conn.put_status(400)
|
||||
@@ -247,7 +299,7 @@ defmodule Mobilizon.Web.ApplicationController do
|
||||
conn
|
||||
|> Plug.Conn.put_status(400)
|
||||
|> json(%{
|
||||
"error" => "invalid_grant",
|
||||
"error" => "expired_token",
|
||||
"error_description" =>
|
||||
dgettext(
|
||||
"errors",
|
||||
|
||||
Reference in New Issue
Block a user