towerops/lib/towerops_web/plugs/webhook_auth.ex
Graham McIntire c7df6a8569
Add CI-triggered mass agent update webhook
POST /api/v1/webhooks/agent-release triggers all connected agents
to self-update via their existing WebSocket channels. Authenticated
with a shared secret (AGENT_WEBHOOK_SECRET env var).

- Add ReleaseChecker.invalidate_cache/0 for fresh GitHub fetch
- Add Agents.list_updatable_agents/0 (enabled + seen < 10min)
- Add Agents.broadcast_mass_update/0 orchestration function
- Add WebhookAuth plug with timing-safe secret comparison
- Add AgentReleaseWebhookController with :webhook pipeline
- Configure AGENT_WEBHOOK_SECRET in dev/test/runtime configs
2026-02-10 13:40:32 -06:00

43 lines
1 KiB
Elixir

defmodule ToweropsWeb.Plugs.WebhookAuth do
@moduledoc """
Plug for authenticating webhook requests using a shared secret.
Expects the Authorization header to contain a Bearer token matching
the configured `agent_webhook_secret`.
Returns 401 Unauthorized if:
- No Authorization header is present
- Token does not match the configured secret
"""
import Phoenix.Controller, only: [json: 2]
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
case get_req_header(conn, "authorization") do
["Bearer " <> token] ->
verify_secret(conn, token)
_ ->
conn
|> put_status(:unauthorized)
|> json(%{error: "Missing or invalid Authorization header"})
|> halt()
end
end
defp verify_secret(conn, token) do
secret = Application.get_env(:towerops, :agent_webhook_secret)
if Plug.Crypto.secure_compare(token, secret) do
conn
else
conn
|> put_status(:unauthorized)
|> json(%{error: "Invalid webhook secret"})
|> halt()
end
end
end