154 lines
4.5 KiB
Elixir
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
|