towerops/lib/towerops/agents/agent_token.ex

154 lines
4.5 KiB
Elixir

defmodule Towerops.Agents.AgentToken do
@moduledoc """
Schema for agent authentication tokens.
Agent tokens are used to authenticate remote SNMP polling agents that run on customer networks.
Tokens are generated with cryptographically secure random bytes (48 bytes = 384 bits) and
stored in plaintext in the database. Tokens can be retrieved and viewed by authorized users.
"""
use Ecto.Schema
import Ecto.Changeset
alias Towerops.Organizations.Organization
# 48 bytes = 384 bits of cryptographically secure randomness
@rand_size 48
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "agent_tokens" do
field :token, :string
field :name, :string
field :last_seen_at, :utc_datetime
field :last_ip, :string
field :enabled, :boolean, default: true
field :is_cloud_poller, :boolean, default: false
field :allow_remote_debug, :boolean, default: false
field :metadata, :map, default: %{}
belongs_to :organization, Organization
timestamps(type: :utc_datetime)
end
@type t :: %__MODULE__{
id: Ecto.UUID.t(),
token: String.t(),
name: String.t(),
last_seen_at: DateTime.t() | nil,
last_ip: String.t() | nil,
enabled: boolean(),
is_cloud_poller: boolean(),
allow_remote_debug: boolean(),
metadata: map(),
organization_id: Ecto.UUID.t() | nil,
organization: Ecto.Association.NotLoaded.t() | Organization.t(),
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}
@doc """
Builds a new agent token with cryptographically secure random bytes.
Returns a tuple of {token_string, changeset}. The token_string is stored in the database
and can be retrieved later for display to authorized users.
For cloud pollers, pass `is_cloud_poller: true` in opts and organization_id will be nil.
## Examples
iex> {token, changeset} = AgentToken.build_token(org_id, "My Agent")
iex> is_binary(token)
true
iex> byte_size(Base.url_decode64!(token, padding: false))
48
iex> {token, changeset} = AgentToken.build_token(nil, "Cloud Poller", is_cloud_poller: true)
iex> changeset.changes.is_cloud_poller
true
"""
def build_token(organization_id, name, opts \\ []) do
is_cloud_poller = Keyword.get(opts, :is_cloud_poller, false)
# Generate 48 bytes (384 bits) of cryptographically secure random data
token = :crypto.strong_rand_bytes(@rand_size)
encoded_token = Base.url_encode64(token, padding: false)
attrs = %{
organization_id: organization_id,
name: name,
token: encoded_token,
is_cloud_poller: is_cloud_poller
}
changeset =
%__MODULE__{}
|> cast(attrs, [:organization_id, :name, :token, :is_cloud_poller])
|> validate_cloud_poller_constraints()
|> validate_required([:name, :token])
|> unique_constraint(:token)
{encoded_token, changeset}
end
defp validate_cloud_poller_constraints(changeset) do
is_cloud_poller = get_field(changeset, :is_cloud_poller)
organization_id = get_field(changeset, :organization_id)
cond do
is_cloud_poller && organization_id != nil ->
add_error(changeset, :organization_id, "must be nil for cloud pollers")
!is_cloud_poller && organization_id == nil ->
add_error(changeset, :organization_id, "can't be blank for non-cloud pollers")
true ->
changeset
end
end
@doc """
Changeset for updating an agent token.
Only allows updating the name and allow_remote_debug fields. Other fields like
the token itself cannot be changed for security reasons.
## Examples
iex> changeset = AgentToken.update_changeset(agent_token, %{name: "New Name"})
iex> changeset.valid?
true
iex> changeset = AgentToken.update_changeset(agent_token, %{name: ""})
iex> changeset.valid?
false
"""
def update_changeset(agent_token, attrs) do
agent_token
|> cast(attrs, [:name, :allow_remote_debug])
|> validate_required([:name])
end
@doc """
Verifies a token by checking if it exists in the database.
Returns {:ok, token} if valid, otherwise {:error, :invalid_token}.
## Examples
iex> AgentToken.verify_token("valid_token")
{:ok, "valid_token"}
iex> AgentToken.verify_token("invalid")
{:error, :invalid_token}
"""
def verify_token(token) when is_binary(token) do
{:ok, token}
end
def verify_token(_), do: {:error, :invalid_token}
end