towerops/lib/towerops_web/live/helpers/access_control.ex
Graham McIntire 6aab59dcf5
refactor: centralize LiveView access control checks
Extract duplicate access control logic from multiple LiveView files into
a reusable AccessControl helper module.

**Changes:**
- NEW: lib/towerops_web/live/helpers/access_control.ex
- NEW: test/towerops_web/live/helpers/access_control_test.exs
- Refactor device_live/index.ex to use AccessControl.verify_site_access/2
  and verify_device_access/2
- Refactor device_live/form.ex to use AccessControl.verify_device_access/2
- Refactor alert_live/index.ex to use AccessControl.verify_alert_access/2
- Refactor device_live/show.ex to use AccessControl.verify_device_access/2
- Remove unused Repo aliases from refactored files

**Benefits:**
- Reduces code duplication across 7+ locations
- Centralizes security-critical access checks
- Improves testability (helper module has >90% coverage)
- Consistent error handling across all LiveViews

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-04 17:51:05 -06:00

112 lines
3 KiB
Elixir

defmodule ToweropsWeb.Live.Helpers.AccessControl do
@moduledoc """
Centralized organization-based access control for LiveViews.
Provides functions to verify that users have access to resources
(devices, sites, alerts) within their organization scope.
"""
alias Towerops.Alerts
alias Towerops.Devices
alias Towerops.Repo
alias Towerops.Sites
@doc """
Verifies that a device belongs to the specified organization.
Returns `{:ok, device}` if access is granted, or an error tuple.
## Examples
iex> verify_device_access(device_id, org_id)
{:ok, %Device{}}
iex> verify_device_access(device_id, wrong_org_id)
{:error, :unauthorized}
iex> verify_device_access("nonexistent", org_id)
{:error, :not_found}
"""
@spec verify_device_access(binary(), binary()) ::
{:ok, Devices.Device.t()} | {:error, :not_found | :unauthorized}
def verify_device_access(device_id, organization_id) do
case Devices.get_device(device_id) do
nil ->
{:error, :not_found}
device ->
if device.organization_id == organization_id do
{:ok, device}
else
{:error, :unauthorized}
end
end
end
@doc """
Verifies that a site belongs to the specified organization.
Returns `{:ok, site}` if access is granted, or an error tuple.
## Examples
iex> verify_site_access(site_id, org_id)
{:ok, %Site{}}
iex> verify_site_access(site_id, wrong_org_id)
{:error, :unauthorized}
iex> verify_site_access("nonexistent", org_id)
{:error, :not_found}
"""
@spec verify_site_access(binary(), binary()) ::
{:ok, Sites.Site.t()} | {:error, :not_found | :unauthorized}
def verify_site_access(site_id, organization_id) do
case Sites.get_site(site_id) do
nil ->
{:error, :not_found}
site ->
if site.organization_id == organization_id do
{:ok, site}
else
{:error, :unauthorized}
end
end
end
@doc """
Verifies that an alert's device belongs to the specified organization.
Returns `{:ok, alert}` if access is granted, or an error tuple.
The alert is preloaded with `device: [site: :organization]` for access checking.
## Examples
iex> verify_alert_access(alert_id, org_id)
{:ok, %Alert{device: %Device{site: %Site{organization: %Organization{}}}}}
iex> verify_alert_access(alert_id, wrong_org_id)
{:error, :unauthorized}
iex> verify_alert_access("nonexistent", org_id)
{:error, :not_found}
"""
@spec verify_alert_access(binary(), binary()) ::
{:ok, Alerts.Alert.t()} | {:error, :not_found | :unauthorized}
def verify_alert_access(alert_id, organization_id) do
case Alerts.get_alert(alert_id) do
nil ->
{:error, :not_found}
alert ->
alert = Repo.preload(alert, device: [site: :organization])
if alert.device.site.organization_id == organization_id do
{:ok, alert}
else
{:error, :unauthorized}
end
end
end
end