Skip to content

Commit ff14198

Browse files
cnkkapata
andauthored
Add rate limiting to activation, activation code request, and TOTP setup endpoints (#6232)
* Implement rate limiting * Increase activation request limit for e2e_test environment * Refactor activation request limit * Update activation request limit condition to include :ce_test environment * Refactor rate limits for activation and TOTP setup in e2e_test environment * Reduce activation request limit interval from 10 minutes to 1 minute * Refactor rate limits and adjust limits and intervals for different environments * Refactor is_super_admin? fn to super_admin? --------- Co-authored-by: Artur Pata <artur.pata@gmail.com>
1 parent 82c7b0e commit ff14198

11 files changed

Lines changed: 135 additions & 31 deletions

File tree

extra/lib/plausible_web/live/verification.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ defmodule PlausibleWeb.Live.Verification do
3535

3636
private = Map.get(socket.private.connect_info, :private, %{})
3737

38-
super_admin? = Plausible.Auth.is_super_admin?(current_user)
38+
super_admin? = Plausible.Auth.super_admin?(current_user)
3939
has_pageviews? = has_pageviews?(site)
4040

4141
custom_url_input? = params["custom_url"] == "true"

lib/plausible/auth/auth.ex

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,33 @@ defmodule Plausible.Auth do
1515

1616
require Logger
1717

18-
if Mix.env() == :e2e_test do
19-
@ip_rate_limit 100_000
20-
@user_rate_limit 100_000
21-
else
22-
@ip_rate_limit 5
23-
@user_rate_limit 5
18+
case Mix.env() do
19+
:e2e_test ->
20+
@ip_rate_limit 100_000
21+
@user_rate_limit 100_000
22+
@activation_limit 100_000
23+
@activation_ip_limit 100_000
24+
@activation_request_limit 100_000
25+
@totp_setup_limit 100_000
26+
@totp_setup_ip_limit 100_000
27+
28+
env when env in [:test, :ce_test] ->
29+
@ip_rate_limit 5
30+
@user_rate_limit 5
31+
@activation_limit 10
32+
@totp_setup_limit 10
33+
@activation_ip_limit 100_000
34+
@totp_setup_ip_limit 100_000
35+
@activation_request_limit 100_000
36+
37+
_ ->
38+
@ip_rate_limit 5
39+
@user_rate_limit 5
40+
@activation_limit 10
41+
@totp_setup_limit 10
42+
@activation_ip_limit 2
43+
@totp_setup_ip_limit 2
44+
@activation_request_limit 5
2445
end
2546

2647
@rate_limits %{
@@ -43,6 +64,36 @@ defmodule Plausible.Auth do
4364
prefix: "password-change:user",
4465
limit: 5,
4566
interval: :timer.minutes(20)
67+
},
68+
activation_ip: %{
69+
prefix: "activation:ip",
70+
limit: @activation_ip_limit,
71+
interval: :timer.minutes(1)
72+
},
73+
activation_user: %{
74+
prefix: "activation:user",
75+
limit: @activation_limit,
76+
interval: :timer.minutes(5)
77+
},
78+
activation_request_ip: %{
79+
prefix: "activation-request:ip",
80+
limit: @activation_request_limit,
81+
interval: :timer.minutes(1)
82+
},
83+
activation_request_user: %{
84+
prefix: "activation-request:user",
85+
limit: @activation_request_limit,
86+
interval: :timer.minutes(10)
87+
},
88+
totp_setup_ip: %{
89+
prefix: "totp-setup:ip",
90+
limit: @totp_setup_ip_limit,
91+
interval: :timer.minutes(1)
92+
},
93+
totp_setup_user: %{
94+
prefix: "totp-setup:user",
95+
limit: @totp_setup_limit,
96+
interval: :timer.minutes(5)
4697
}
4798
}
4899

@@ -181,14 +232,14 @@ defmodule Plausible.Auth do
181232
end
182233

183234
on_ee do
184-
def is_super_admin?(nil), do: false
185-
def is_super_admin?(%Plausible.Auth.User{id: id}), do: is_super_admin?(id)
235+
def super_admin?(nil), do: false
236+
def super_admin?(%Plausible.Auth.User{id: id}), do: super_admin?(id)
186237

187-
def is_super_admin?(user_id) when is_integer(user_id) do
238+
def super_admin?(user_id) when is_integer(user_id) do
188239
user_id in Application.get_env(:plausible, :super_admin_user_ids)
189240
end
190241
else
191-
def is_super_admin?(_), do: always(false)
242+
def super_admin?(_), do: always(false)
192243
end
193244

194245
@spec list_api_keys(Auth.User.t(), Teams.Team.t() | nil) :: [Auth.ApiKey.t()]

lib/plausible/sites.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ defmodule Plausible.Sites do
459459
include_consolidated? = Keyword.fetch!(opts, :include_consolidated?)
460460

461461
site =
462-
if :super_admin in roles and Plausible.Auth.is_super_admin?(user.id) do
462+
if :super_admin in roles and Plausible.Auth.super_admin?(user.id) do
463463
get_by_domain!(domain, include_consolidated?: include_consolidated?)
464464
else
465465
user.id
@@ -475,7 +475,7 @@ defmodule Plausible.Sites do
475475
roles = Keyword.fetch!(opts, :roles)
476476
include_consolidated? = Keyword.fetch!(opts, :include_consolidated?)
477477

478-
if :super_admin in roles and Plausible.Auth.is_super_admin?(user.id) do
478+
if :super_admin in roles and Plausible.Auth.super_admin?(user.id) do
479479
get_by_domain(domain, include_consolidated?: include_consolidated?)
480480
else
481481
user.id

lib/plausible_web/controllers/api/internal_controller.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ defmodule PlausibleWeb.Api.InternalController do
3232
site <- Sites.get_by_domain(domain),
3333
true <-
3434
Plausible.Teams.Memberships.has_editor_access?(site, user) ||
35-
Auth.is_super_admin?(user_id),
35+
Auth.super_admin?(user_id),
3636
{:ok, mod} <- Map.fetch(@features, feature),
3737
{:ok, _site} <- mod.toggle(site, user, override: false) do
3838
json(conn, "ok")

lib/plausible_web/controllers/auth_controller.ex

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,20 @@ defmodule PlausibleWeb.AuthController do
123123
def activate(conn, %{"code" => code}) do
124124
user = conn.assigns[:current_user]
125125

126+
with :ok <- Auth.rate_limit(:activation_ip, conn),
127+
:ok <- Auth.rate_limit(:activation_user, user) do
128+
do_activate(conn, user, code)
129+
else
130+
{:error, {:rate_limit, _}} ->
131+
render_error(
132+
conn,
133+
429,
134+
"Too many activation attempts. Wait a few minutes before trying again."
135+
)
136+
end
137+
end
138+
139+
defp do_activate(conn, user, code) do
126140
has_any_invitations? = Plausible.Teams.Users.has_sites?(user, include_pending?: true)
127141
has_any_memberships? = Plausible.Teams.Users.has_sites?(user, include_pending?: false)
128142

@@ -167,11 +181,20 @@ defmodule PlausibleWeb.AuthController do
167181

168182
def request_activation_code(conn, _params) do
169183
user = conn.assigns.current_user
170-
Auth.EmailVerification.issue_code(user)
171184

172-
conn
173-
|> put_flash(:success, "Activation code was sent to #{user.email}")
174-
|> redirect(to: Routes.auth_path(conn, :activate_form))
185+
with :ok <- Auth.rate_limit(:activation_request_ip, conn),
186+
:ok <- Auth.rate_limit(:activation_request_user, user) do
187+
Auth.EmailVerification.issue_code(user)
188+
189+
conn
190+
|> put_flash(:success, "Activation code was sent to #{user.email}")
191+
|> redirect(to: Routes.auth_path(conn, :activate_form))
192+
else
193+
{:error, {:rate_limit, _}} ->
194+
conn
195+
|> put_flash(:error, "Too many code requests. Please wait before requesting another.")
196+
|> redirect(to: Routes.auth_path(conn, :activate_form))
197+
end
175198
end
176199

177200
def password_reset_request_form(conn, _) do
@@ -396,11 +419,21 @@ defmodule PlausibleWeb.AuthController do
396419
end
397420

398421
def verify_2fa_setup(conn, %{"code" => code}) do
399-
case Auth.TOTP.enable(conn.assigns.current_user, code) do
400-
{:ok, _, %{recovery_codes: codes}} ->
401-
conn
402-
|> put_flash(:success, "Two-Factor Authentication is fully enabled")
403-
|> render("generate_2fa_recovery_codes.html", recovery_codes: codes, from_setup: true)
422+
user = conn.assigns.current_user
423+
424+
with :ok <- Auth.rate_limit(:totp_setup_ip, conn),
425+
:ok <- Auth.rate_limit(:totp_setup_user, user),
426+
{:ok, _, %{recovery_codes: codes}} <- Auth.TOTP.enable(user, code) do
427+
conn
428+
|> put_flash(:success, "Two-Factor Authentication is fully enabled")
429+
|> render("generate_2fa_recovery_codes.html", recovery_codes: codes, from_setup: true)
430+
else
431+
{:error, {:rate_limit, _}} ->
432+
render_error(
433+
conn,
434+
429,
435+
"Too many attempts. Wait a minute before trying again."
436+
)
404437

405438
{:error, :invalid_code} ->
406439
conn

lib/plausible_web/controllers/stats_controller.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ defmodule PlausibleWeb.StatsController do
6060
consolidated_view? = Plausible.Sites.consolidated?(site)
6161

6262
exploration_available? =
63-
on_ee(do: Plausible.Auth.is_super_admin?(current_user), else: false)
63+
on_ee(do: Plausible.Auth.super_admin?(current_user), else: false)
6464

6565
{exploration_journey_end_event, exploration_max_journey_steps} =
6666
on_ee(
@@ -483,7 +483,7 @@ defmodule PlausibleWeb.StatsController do
483483
flags = get_flags(current_user, shared_link.site)
484484

485485
exploration_available? =
486-
on_ee(do: Plausible.Auth.is_super_admin?(current_user), else: false)
486+
on_ee(do: Plausible.Auth.super_admin?(current_user), else: false)
487487

488488
{exploration_journey_end_event, exploration_max_journey_steps} =
489489
on_ee(

lib/plausible_web/plugs/authorize_public_api.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
7676
team_role_result = Plausible.Teams.Memberships.team_role(team, api_key.user)
7777

7878
cond do
79-
Auth.is_super_admin?(api_key.user) ->
79+
Auth.super_admin?(api_key.user) ->
8080
:pass
8181

8282
team_role_result == {:ok, :guest} ->
@@ -263,7 +263,7 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
263263
team = Repo.preload(site, :team).team
264264

265265
is_member? = Plausible.Teams.Memberships.site_member?(site, api_key.user)
266-
is_super_admin? = Auth.is_super_admin?(api_key.user_id)
266+
is_super_admin? = Auth.super_admin?(api_key.user_id)
267267

268268
cond do
269269
Plausible.Sites.consolidated?(site) && !allow_consolidated_views ->
@@ -291,7 +291,7 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
291291

292292
defp verify_team_access(api_key, team, feature) do
293293
is_member? = Plausible.Teams.Memberships.team_member?(team, api_key.user)
294-
is_super_admin? = Auth.is_super_admin?(api_key.user_id)
294+
is_super_admin? = Auth.super_admin?(api_key.user_id)
295295

296296
cond do
297297
is_super_admin? ->

lib/plausible_web/plugs/authorize_site_access.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccess do
8888
membership_role ->
8989
membership_role
9090

91-
Plausible.Auth.is_super_admin?(current_user) ->
91+
Plausible.Auth.super_admin?(current_user) ->
9292
:super_admin
9393

9494
site.public ->

lib/plausible_web/plugs/super_admin_only_plug.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ defmodule PlausibleWeb.SuperAdminOnlyPlug do
1212
def call(conn, _opts) do
1313
current_user = conn.assigns[:current_user]
1414

15-
if current_user && Plausible.Auth.is_super_admin?(current_user) do
15+
if current_user && Plausible.Auth.super_admin?(current_user) do
1616
conn
1717
else
1818
conn

lib/plausible_web/templates/layout/_header.html.heex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
<ul class="flex items-center gap-2 w-full sm:w-auto">
3434
<li :if={
3535
ee?() && @conn.assigns[:site] &&
36-
Plausible.Auth.is_super_admin?(@conn.assigns[:current_user])
36+
Plausible.Auth.super_admin?(@conn.assigns[:current_user])
3737
}>
3838
<.styled_link
3939
class="text-sm font-medium"

0 commit comments

Comments
 (0)