diff --git a/extra/lib/plausible_web/live/verification.ex b/extra/lib/plausible_web/live/verification.ex index 97e03fc3c47d..0fc148d3a2b0 100644 --- a/extra/lib/plausible_web/live/verification.ex +++ b/extra/lib/plausible_web/live/verification.ex @@ -35,7 +35,7 @@ defmodule PlausibleWeb.Live.Verification do private = Map.get(socket.private.connect_info, :private, %{}) - super_admin? = Plausible.Auth.is_super_admin?(current_user) + super_admin? = Plausible.Auth.super_admin?(current_user) has_pageviews? = has_pageviews?(site) custom_url_input? = params["custom_url"] == "true" diff --git a/lib/plausible/auth/auth.ex b/lib/plausible/auth/auth.ex index 91606dcde5da..aedf5aa28b28 100644 --- a/lib/plausible/auth/auth.ex +++ b/lib/plausible/auth/auth.ex @@ -15,12 +15,33 @@ defmodule Plausible.Auth do require Logger - if Mix.env() == :e2e_test do - @ip_rate_limit 100_000 - @user_rate_limit 100_000 - else - @ip_rate_limit 5 - @user_rate_limit 5 + case Mix.env() do + :e2e_test -> + @ip_rate_limit 100_000 + @user_rate_limit 100_000 + @activation_limit 100_000 + @activation_ip_limit 100_000 + @activation_request_limit 100_000 + @totp_setup_limit 100_000 + @totp_setup_ip_limit 100_000 + + env when env in [:test, :ce_test] -> + @ip_rate_limit 5 + @user_rate_limit 5 + @activation_limit 10 + @totp_setup_limit 10 + @activation_ip_limit 100_000 + @totp_setup_ip_limit 100_000 + @activation_request_limit 100_000 + + _ -> + @ip_rate_limit 5 + @user_rate_limit 5 + @activation_limit 10 + @totp_setup_limit 10 + @activation_ip_limit 2 + @totp_setup_ip_limit 2 + @activation_request_limit 5 end @rate_limits %{ @@ -43,6 +64,36 @@ defmodule Plausible.Auth do prefix: "password-change:user", limit: 5, interval: :timer.minutes(20) + }, + activation_ip: %{ + prefix: "activation:ip", + limit: @activation_ip_limit, + interval: :timer.minutes(1) + }, + activation_user: %{ + prefix: "activation:user", + limit: @activation_limit, + interval: :timer.minutes(5) + }, + activation_request_ip: %{ + prefix: "activation-request:ip", + limit: @activation_request_limit, + interval: :timer.minutes(1) + }, + activation_request_user: %{ + prefix: "activation-request:user", + limit: @activation_request_limit, + interval: :timer.minutes(10) + }, + totp_setup_ip: %{ + prefix: "totp-setup:ip", + limit: @totp_setup_ip_limit, + interval: :timer.minutes(1) + }, + totp_setup_user: %{ + prefix: "totp-setup:user", + limit: @totp_setup_limit, + interval: :timer.minutes(5) } } @@ -181,14 +232,14 @@ defmodule Plausible.Auth do end on_ee do - def is_super_admin?(nil), do: false - def is_super_admin?(%Plausible.Auth.User{id: id}), do: is_super_admin?(id) + def super_admin?(nil), do: false + def super_admin?(%Plausible.Auth.User{id: id}), do: super_admin?(id) - def is_super_admin?(user_id) when is_integer(user_id) do + def super_admin?(user_id) when is_integer(user_id) do user_id in Application.get_env(:plausible, :super_admin_user_ids) end else - def is_super_admin?(_), do: always(false) + def super_admin?(_), do: always(false) end @spec list_api_keys(Auth.User.t(), Teams.Team.t() | nil) :: [Auth.ApiKey.t()] diff --git a/lib/plausible/sites.ex b/lib/plausible/sites.ex index af057e81d20d..ab13d9db2ab1 100644 --- a/lib/plausible/sites.ex +++ b/lib/plausible/sites.ex @@ -459,7 +459,7 @@ defmodule Plausible.Sites do include_consolidated? = Keyword.fetch!(opts, :include_consolidated?) site = - if :super_admin in roles and Plausible.Auth.is_super_admin?(user.id) do + if :super_admin in roles and Plausible.Auth.super_admin?(user.id) do get_by_domain!(domain, include_consolidated?: include_consolidated?) else user.id @@ -475,7 +475,7 @@ defmodule Plausible.Sites do roles = Keyword.fetch!(opts, :roles) include_consolidated? = Keyword.fetch!(opts, :include_consolidated?) - if :super_admin in roles and Plausible.Auth.is_super_admin?(user.id) do + if :super_admin in roles and Plausible.Auth.super_admin?(user.id) do get_by_domain(domain, include_consolidated?: include_consolidated?) else user.id diff --git a/lib/plausible_web/controllers/api/internal_controller.ex b/lib/plausible_web/controllers/api/internal_controller.ex index bcd0f42c0d91..821fe58e58b6 100644 --- a/lib/plausible_web/controllers/api/internal_controller.ex +++ b/lib/plausible_web/controllers/api/internal_controller.ex @@ -32,7 +32,7 @@ defmodule PlausibleWeb.Api.InternalController do site <- Sites.get_by_domain(domain), true <- Plausible.Teams.Memberships.has_editor_access?(site, user) || - Auth.is_super_admin?(user_id), + Auth.super_admin?(user_id), {:ok, mod} <- Map.fetch(@features, feature), {:ok, _site} <- mod.toggle(site, user, override: false) do json(conn, "ok") diff --git a/lib/plausible_web/controllers/auth_controller.ex b/lib/plausible_web/controllers/auth_controller.ex index 9fbd6237677d..5dc6c251343d 100644 --- a/lib/plausible_web/controllers/auth_controller.ex +++ b/lib/plausible_web/controllers/auth_controller.ex @@ -123,6 +123,20 @@ defmodule PlausibleWeb.AuthController do def activate(conn, %{"code" => code}) do user = conn.assigns[:current_user] + with :ok <- Auth.rate_limit(:activation_ip, conn), + :ok <- Auth.rate_limit(:activation_user, user) do + do_activate(conn, user, code) + else + {:error, {:rate_limit, _}} -> + render_error( + conn, + 429, + "Too many activation attempts. Wait a few minutes before trying again." + ) + end + end + + defp do_activate(conn, user, code) do has_any_invitations? = Plausible.Teams.Users.has_sites?(user, include_pending?: true) has_any_memberships? = Plausible.Teams.Users.has_sites?(user, include_pending?: false) @@ -167,11 +181,20 @@ defmodule PlausibleWeb.AuthController do def request_activation_code(conn, _params) do user = conn.assigns.current_user - Auth.EmailVerification.issue_code(user) - conn - |> put_flash(:success, "Activation code was sent to #{user.email}") - |> redirect(to: Routes.auth_path(conn, :activate_form)) + with :ok <- Auth.rate_limit(:activation_request_ip, conn), + :ok <- Auth.rate_limit(:activation_request_user, user) do + Auth.EmailVerification.issue_code(user) + + conn + |> put_flash(:success, "Activation code was sent to #{user.email}") + |> redirect(to: Routes.auth_path(conn, :activate_form)) + else + {:error, {:rate_limit, _}} -> + conn + |> put_flash(:error, "Too many code requests. Please wait before requesting another.") + |> redirect(to: Routes.auth_path(conn, :activate_form)) + end end def password_reset_request_form(conn, _) do @@ -396,11 +419,21 @@ defmodule PlausibleWeb.AuthController do end def verify_2fa_setup(conn, %{"code" => code}) do - case Auth.TOTP.enable(conn.assigns.current_user, code) do - {:ok, _, %{recovery_codes: codes}} -> - conn - |> put_flash(:success, "Two-Factor Authentication is fully enabled") - |> render("generate_2fa_recovery_codes.html", recovery_codes: codes, from_setup: true) + user = conn.assigns.current_user + + with :ok <- Auth.rate_limit(:totp_setup_ip, conn), + :ok <- Auth.rate_limit(:totp_setup_user, user), + {:ok, _, %{recovery_codes: codes}} <- Auth.TOTP.enable(user, code) do + conn + |> put_flash(:success, "Two-Factor Authentication is fully enabled") + |> render("generate_2fa_recovery_codes.html", recovery_codes: codes, from_setup: true) + else + {:error, {:rate_limit, _}} -> + render_error( + conn, + 429, + "Too many attempts. Wait a minute before trying again." + ) {:error, :invalid_code} -> conn diff --git a/lib/plausible_web/controllers/stats_controller.ex b/lib/plausible_web/controllers/stats_controller.ex index 7723a088736c..5045623d6160 100644 --- a/lib/plausible_web/controllers/stats_controller.ex +++ b/lib/plausible_web/controllers/stats_controller.ex @@ -60,7 +60,7 @@ defmodule PlausibleWeb.StatsController do consolidated_view? = Plausible.Sites.consolidated?(site) exploration_available? = - on_ee(do: Plausible.Auth.is_super_admin?(current_user), else: false) + on_ee(do: Plausible.Auth.super_admin?(current_user), else: false) consolidated_view_available? = on_ee(do: Plausible.ConsolidatedView.ok_to_display?(site.team), else: false) @@ -473,7 +473,7 @@ defmodule PlausibleWeb.StatsController do flags = get_flags(current_user, shared_link.site) exploration_available? = - on_ee(do: Plausible.Auth.is_super_admin?(current_user), else: false) + on_ee(do: Plausible.Auth.super_admin?(current_user), else: false) limited_to_segment_id = if Plausible.Site.SharedLink.limited_to_segment?(shared_link) do diff --git a/lib/plausible_web/plugs/authorize_public_api.ex b/lib/plausible_web/plugs/authorize_public_api.ex index e0efb9c0e1f6..9dde8e3b3be0 100644 --- a/lib/plausible_web/plugs/authorize_public_api.ex +++ b/lib/plausible_web/plugs/authorize_public_api.ex @@ -76,7 +76,7 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do team_role_result = Plausible.Teams.Memberships.team_role(team, api_key.user) cond do - Auth.is_super_admin?(api_key.user) -> + Auth.super_admin?(api_key.user) -> :pass team_role_result == {:ok, :guest} -> @@ -263,7 +263,7 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do team = Repo.preload(site, :team).team is_member? = Plausible.Teams.Memberships.site_member?(site, api_key.user) - is_super_admin? = Auth.is_super_admin?(api_key.user_id) + is_super_admin? = Auth.super_admin?(api_key.user_id) cond do Plausible.Sites.consolidated?(site) && !allow_consolidated_views -> @@ -291,7 +291,7 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do defp verify_team_access(api_key, team, feature) do is_member? = Plausible.Teams.Memberships.team_member?(team, api_key.user) - is_super_admin? = Auth.is_super_admin?(api_key.user_id) + is_super_admin? = Auth.super_admin?(api_key.user_id) cond do is_super_admin? -> diff --git a/lib/plausible_web/plugs/authorize_site_access.ex b/lib/plausible_web/plugs/authorize_site_access.ex index 970e2883b53d..dcb21471c5fa 100644 --- a/lib/plausible_web/plugs/authorize_site_access.ex +++ b/lib/plausible_web/plugs/authorize_site_access.ex @@ -88,7 +88,7 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccess do membership_role -> membership_role - Plausible.Auth.is_super_admin?(current_user) -> + Plausible.Auth.super_admin?(current_user) -> :super_admin site.public -> diff --git a/lib/plausible_web/plugs/super_admin_only_plug.ex b/lib/plausible_web/plugs/super_admin_only_plug.ex index cb67150e99b2..d31e4982d95a 100644 --- a/lib/plausible_web/plugs/super_admin_only_plug.ex +++ b/lib/plausible_web/plugs/super_admin_only_plug.ex @@ -12,7 +12,7 @@ defmodule PlausibleWeb.SuperAdminOnlyPlug do def call(conn, _opts) do current_user = conn.assigns[:current_user] - if current_user && Plausible.Auth.is_super_admin?(current_user) do + if current_user && Plausible.Auth.super_admin?(current_user) do conn else conn diff --git a/lib/plausible_web/templates/layout/_header.html.heex b/lib/plausible_web/templates/layout/_header.html.heex index 52989fcdaffa..58a0ec89fae5 100644 --- a/lib/plausible_web/templates/layout/_header.html.heex +++ b/lib/plausible_web/templates/layout/_header.html.heex @@ -33,7 +33,7 @@