towerops/lib/towerops_web/live/admin/security_live/index.ex
Graham McIntire 6fa0b791f2 fix: M3, M6, M7, M12 — open redirect, overfetch, missing constraint, admin defense-in-depth
- M3: Replace blacklist-based valid_return_path? with whitelist of known app
  route prefixes, plus URI decoding to prevent %2f encoding bypasses
- M6: Replace full-org device/link load in get_node_detail with targeted
  join query filtering by discovered node fields
- M7: Add partial unique index on (organization_id, email) for pending
  invitations (WHERE accepted_at IS NULL)
- M12: Add on_mount superuser verification to all 7 admin LiveViews for
  defense-in-depth
2026-05-12 12:46:16 -05:00

132 lines
3.8 KiB
Elixir

defmodule ToweropsWeb.Admin.SecurityLive.Index do
@moduledoc """
Admin page for managing IP-based brute force protection.
Provides two main functions:
1. Allowlist management - Add/remove IPs and CIDR ranges that bypass protection
2. Denylist management - View and manually unblock banned IPs
"""
use ToweropsWeb, :live_view
alias Towerops.Security.BruteForce
on_mount {ToweropsWeb.UserAuth, :require_superuser}
@impl true
def mount(_params, _session, socket) do
_ =
if connected?(socket) do
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "security:whitelist")
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "security:blocks")
end
{:ok,
socket
|> assign(:page_title, t("Security - IP Access Control"))
|> assign(:current_tab, "whitelist")}
end
@impl true
def handle_params(params, _url, socket) do
tab = Map.get(params, "tab", "whitelist")
filter = Map.get(params, "filter", "all")
{:noreply,
socket
|> assign(:current_tab, tab)
|> assign(:filter, filter)
|> load_data()}
end
@impl true
def handle_event("change_tab", %{"tab" => tab}, socket) do
{:noreply, push_patch(socket, to: ~p"/admin/security?tab=#{tab}")}
end
def handle_event("change_filter", %{"filter" => filter}, socket) do
{:noreply, push_patch(socket, to: ~p"/admin/security?tab=#{socket.assigns.current_tab}&filter=#{filter}")}
end
def handle_event("show_whitelist_form", _params, socket) do
{:noreply,
socket
|> assign(:show_whitelist_form, true)
|> assign(:whitelist_form, to_form(%{}))}
end
def handle_event("hide_whitelist_form", _params, socket) do
{:noreply, assign(socket, :show_whitelist_form, false)}
end
def handle_event("add_whitelist", %{"whitelist" => params}, socket) do
case BruteForce.add_to_whitelist(
params["ip_or_cidr"],
params["description"],
socket.assigns.current_scope.user
) do
{:ok, _entry} ->
{:noreply,
socket
|> put_flash(:info, t("IP/CIDR added to allowlist"))
|> assign(:show_whitelist_form, false)
|> load_data()}
{:error, changeset} ->
{:noreply,
socket
|> assign(:whitelist_form, to_form(Map.put(changeset, :action, :validate)))
|> put_flash(:error, t("Failed to add to allowlist"))}
end
end
def handle_event("remove_whitelist", %{"id" => id}, socket) do
case BruteForce.remove_from_whitelist(id) do
:ok ->
{:noreply,
socket
|> put_flash(:info, t("Removed from allowlist"))
|> load_data()}
{:error, _} ->
{:noreply, put_flash(socket, :error, t("Failed to remove from allowlist"))}
end
end
def handle_event("unblock_ip", %{"ip" => ip_address}, socket) do
case BruteForce.manually_unblock(ip_address) do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, t("IP %{ip_address} unblocked", ip_address: ip_address))
|> load_data()}
{:error, _} ->
{:noreply, put_flash(socket, :error, t("Failed to unblock IP"))}
end
end
@impl true
def handle_info(:whitelist_updated, socket) do
{:noreply, load_data(socket)}
end
def handle_info(:blocks_updated, socket) do
{:noreply, load_data(socket)}
end
defp load_data(socket) do
whitelist = BruteForce.list_whitelist()
blocked_ips =
case socket.assigns[:filter] || "all" do
"permanent" -> BruteForce.list_blocked_ips(:permanent)
"temporary" -> BruteForce.list_blocked_ips(:temporary)
_ -> BruteForce.list_blocked_ips(:all)
end
socket
|> assign(:whitelist, whitelist)
|> assign(:blocked_ips, blocked_ips)
|> assign(:show_whitelist_form, Map.get(socket.assigns, :show_whitelist_form, false))
end
end