towerops/lib/towerops_web/plugs/agent_auth.ex
Graham McIntire f9b575816a
Fix database ownership issues in agent auth tests
- Update AgentAuth plug to use synchronous heartbeat updates in test env
- Refactor async heartbeat logic to reduce nesting (Credo)
- Remove Process.sleep from tests (no longer needed with sync updates)

This fixes 'owner exited' database errors in the test suite by avoiding
async Task spawns during tests, which caused DB connection ownership
conflicts with Ecto's sandbox mode.
2026-01-13 13:16:54 -06:00

81 lines
2.1 KiB
Elixir

defmodule ToweropsWeb.Plugs.AgentAuth do
@moduledoc """
Plug for authenticating remote agent API requests.
Validates Bearer tokens in the Authorization header and assigns the agent token
to the connection. Also updates the agent's last_seen_at timestamp asynchronously.
"""
import Phoenix.Controller
import Plug.Conn
alias Ecto.Adapters.SQL.Sandbox
alias Towerops.Agents
@doc """
Initializes the plug with options.
"""
def init(opts), do: opts
@doc """
Validates the agent token from the Authorization header.
If valid, assigns :current_agent_token to the connection and updates heartbeat.
If invalid or missing, returns 401 Unauthorized and halts the connection.
"""
def call(conn, _opts) do
case get_req_header(conn, "authorization") do
["Bearer " <> token] ->
case Agents.verify_agent_token(token) do
{:ok, agent_token} ->
conn
|> assign(:current_agent_token, agent_token)
|> update_heartbeat(agent_token)
{:error, _} ->
unauthorized(conn)
end
_ ->
unauthorized(conn)
end
end
defp update_heartbeat(conn, agent_token) do
ip = to_string(:inet_parse.ntoa(conn.remote_ip))
# In test environment, update synchronously to avoid DB ownership issues
# In production, update asynchronously for better performance
if Mix.env() == :test do
_ = Agents.update_agent_token_heartbeat(agent_token.id, ip)
else
update_heartbeat_async(agent_token.id, ip)
end
conn
end
defp update_heartbeat_async(agent_token_id, ip) do
parent = self()
Task.start(fn ->
# Allow this task to use the test database sandbox (shouldn't happen in prod)
allow_sandbox(parent)
Agents.update_agent_token_heartbeat(agent_token_id, ip)
end)
end
defp allow_sandbox(parent) do
if Application.get_env(:towerops, :sql_sandbox) do
Sandbox.allow(Towerops.Repo, parent, self())
end
end
defp unauthorized(conn) do
conn
|> put_status(:unauthorized)
|> put_view(json: ToweropsWeb.ErrorJSON)
|> render(:"401")
|> halt()
end
end