From 8f52d8785445648570a2b15121287dcbbf298f2f Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Thu, 5 Mar 2026 14:09:20 -0600 Subject: [PATCH] feat: add rate limiting to admin endpoints (100 req/min) - Add :admin type to RateLimit plug with 100 requests per minute per IP - Apply rate limiting to all /admin routes (LiveView and controller actions) - Protects superuser endpoints from abuse and brute force attempts - Aligns with existing auth (10 req/min) and API (1000 req/min) limits --- CHANGELOG.txt | 8 ++++++++ lib/towerops_web/plugs/rate_limit.ex | 13 +++++++++---- lib/towerops_web/router.ex | 8 ++++++-- priv/static/changelog.txt | 1 + test/towerops_web/plugs/rate_limit_test.exs | 2 +- 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ab5db0de..2cf0bb46 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,4 +1,12 @@ 2026-03-05 +feat: add rate limiting to admin endpoints (100 req/min) + - Add :admin type to RateLimit plug with 100 requests per minute per IP + - Apply rate limiting to all /admin routes (LiveView and controller actions) + - Protects superuser endpoints from abuse and brute force attempts + - Aligns with existing auth (10 req/min) and API (1000 req/min) rate limits +Files: lib/towerops_web/plugs/rate_limit.ex, + lib/towerops_web/router.ex + feat: add TowerOps suffix to all page titles for better tab identification - Page titles now show "Page Title | TowerOps" format - Helps users identify TowerOps tabs when multiple tabs are open diff --git a/lib/towerops_web/plugs/rate_limit.ex b/lib/towerops_web/plugs/rate_limit.ex index 0844db82..1c36b131 100644 --- a/lib/towerops_web/plugs/rate_limit.ex +++ b/lib/towerops_web/plugs/rate_limit.ex @@ -4,7 +4,8 @@ defmodule ToweropsWeb.Plugs.RateLimit do Applies different rate limits based on the type of endpoint: - Auth endpoints (login, register, password reset): 10 requests per minute per IP - - API endpoints: 100 requests per minute per IP + - API endpoints: 1000 requests per minute per IP + - Admin endpoints: 100 requests per minute per IP (superuser access) Uses the client's real IP address from X-Forwarded-For or X-Real-IP headers when behind a proxy/load balancer. @@ -21,13 +22,16 @@ defmodule ToweropsWeb.Plugs.RateLimit do @api_limit 1000 @api_period to_timeout(minute: 1) - @type limit_type :: :auth | :api + @admin_limit 100 + @admin_period to_timeout(minute: 1) + + @type limit_type :: :auth | :api | :admin def init(opts) do type = Keyword.get(opts, :type, :api) - if type not in [:auth, :api] do - raise ArgumentError, "RateLimit plug :type must be :auth or :api, got: #{inspect(type)}" + if type not in [:auth, :api, :admin] do + raise ArgumentError, "RateLimit plug :type must be :auth, :api, or :admin, got: #{inspect(type)}" end type @@ -68,6 +72,7 @@ defmodule ToweropsWeb.Plugs.RateLimit do defp limits_for(:auth), do: {@auth_limit, @auth_period} defp limits_for(:api), do: {@api_limit, @api_period} + defp limits_for(:admin), do: {@admin_limit, @admin_period} defp get_remote_ip(conn) do # Try X-Forwarded-For first (set by Traefik and other proxies) diff --git a/lib/towerops_web/router.ex b/lib/towerops_web/router.ex index 66412506..5ae2ac58 100644 --- a/lib/towerops_web/router.ex +++ b/lib/towerops_web/router.ex @@ -275,8 +275,12 @@ defmodule ToweropsWeb.Router do ## Admin routes (superuser only) + pipeline :admin_rate_limit do + plug RateLimit, type: :admin + end + scope "/", ToweropsWeb do - pipe_through [:browser, :require_authenticated_user, :require_superuser] + pipe_through [:browser, :require_authenticated_user, :require_superuser, :admin_rate_limit] post "/admin/impersonate/:user_id", AdminController, :start_impersonate delete "/admin/impersonate", AdminController, :stop_impersonate @@ -292,7 +296,7 @@ defmodule ToweropsWeb.Router do {ToweropsWeb.UserAuth, :handle_policy_consent} ] do scope "/admin", ToweropsWeb.Admin do - pipe_through [:browser, :require_authenticated_user, :require_superuser] + pipe_through [:browser, :require_authenticated_user, :require_superuser, :admin_rate_limit] live "/", DashboardLive, :index live "/users", UserLive.Index, :index diff --git a/priv/static/changelog.txt b/priv/static/changelog.txt index 4ea7e9dc..ad133f78 100644 --- a/priv/static/changelog.txt +++ b/priv/static/changelog.txt @@ -2,6 +2,7 @@ * Enhanced security for API endpoints and file uploads * Improved validation for mobile device registration * Better protection against potential DoS attacks +* Added rate limiting to admin endpoints * Strengthened authentication requirements for sensitive operations * Enhanced error messages to prevent information disclosure diff --git a/test/towerops_web/plugs/rate_limit_test.exs b/test/towerops_web/plugs/rate_limit_test.exs index f526afe5..421f164c 100644 --- a/test/towerops_web/plugs/rate_limit_test.exs +++ b/test/towerops_web/plugs/rate_limit_test.exs @@ -31,7 +31,7 @@ defmodule ToweropsWeb.Plugs.RateLimitTest do end test "raises on invalid type" do - assert_raise ArgumentError, ~r/must be :auth or :api/, fn -> + assert_raise ArgumentError, ~r/must be :auth, :api, or :admin/, fn -> RateLimit.init(type: :invalid) end end