- 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.
81 lines
2.1 KiB
Elixir
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
|