towerops/lib/towerops_web/controllers/api/v1/members_controller.ex
Graham McIntire ca7bb75472 fix: 11 critical security/correctness bugs from code audit
- C1: Replace innerHTML template literals with DOM textContent in sites_map.ts
- C2: Derive user_id from current_scope instead of client params in policy consent
- C3: Fix broken halt() in GDPR data export (orphaned _ = ... halt())
- C4: Add owner/admin authorization check to MembersController update/delete
- C5: Require HMAC signature on agent release webhook (was optional)
- C6: Fix Repo.all_by/2 (not a real Ecto function) → Repo.all with query
- C7: Move auth exemption to before_send callback in BruteForceProtection
- C8: Block </style>/<script injection in status page custom_css validation
- C9: Create secrets.example.yaml with placeholders; secrets.yaml already gitignored
- C10: Load Stripe key from STRIPE_SECRET_KEY env var instead of hardcoded value
- C13: Remove credentials from PubSub backup broadcast; channel resolves them

C11 (no force_ssl): by design — Cloudflared terminates TLS
C12 (device quota race): false positive — FOR UPDATE serializes correctly
2026-05-12 10:20:52 -05:00

101 lines
2.9 KiB
Elixir

defmodule ToweropsWeb.Api.V1.MembersController do
@moduledoc "API controller for organization members."
use ToweropsWeb, :controller
import ToweropsWeb.Api.ErrorHelpers, only: [translate_errors: 1]
alias Towerops.Organizations
def index(conn, _params) do
organization_id = conn.assigns.current_organization_id
members =
organization_id
|> Organizations.list_organization_members()
|> Enum.map(&format_member/1)
json(conn, %{data: members})
end
def update(conn, %{"id" => user_id, "role" => role}) do
organization_id = conn.assigns.current_organization_id
case authorize_member_management(conn, organization_id) do
{:ok, conn} ->
case Organizations.update_member_role(organization_id, user_id, role) do
{:ok, membership} ->
membership = Towerops.Repo.preload(membership, :user)
json(conn, %{data: format_member(membership)})
{:error, :cannot_change_owner_role} ->
conn |> put_status(:forbidden) |> json(%{error: "Cannot change owner role"})
{:error, :not_found} ->
conn |> put_status(:not_found) |> json(%{error: "Member not found"})
{:error, changeset} ->
conn |> put_status(:unprocessable_entity) |> json(%{errors: translate_errors(changeset)})
end
{:halt, conn} ->
conn
end
end
def update(conn, _params) do
conn |> put_status(:bad_request) |> json(%{error: "Missing 'role' parameter"})
end
def delete(conn, %{"id" => user_id}) do
organization_id = conn.assigns.current_organization_id
case authorize_member_management(conn, organization_id) do
{:ok, conn} ->
case Organizations.remove_member(organization_id, user_id) do
{:ok, _} -> conn |> put_status(:no_content) |> send_resp(204, "")
{:error, :cannot_remove_owner} -> conn |> put_status(:forbidden) |> json(%{error: "Cannot remove owner"})
{:error, :not_found} -> conn |> put_status(:not_found) |> json(%{error: "Member not found"})
end
{:halt, conn} ->
conn
end
end
defp authorize_member_management(conn, organization_id) do
user = conn.assigns[:current_user]
if user do
membership = Organizations.get_membership(organization_id, user.id)
if membership && membership.role in [:owner, :admin] do
{:ok, conn}
else
conn =
conn
|> put_status(:forbidden)
|> json(%{error: "Only owners and admins can manage members"})
|> halt()
{:halt, conn}
end
else
conn =
conn
|> put_status(:unauthorized)
|> json(%{error: "Authentication required"})
|> halt()
{:halt, conn}
end
end
defp format_member(membership) do
%{
id: membership.user_id,
email: membership.user.email,
role: membership.role,
inserted_at: membership.inserted_at
}
end
end