agent token changes
This commit is contained in:
parent
aa62cc7349
commit
b8bd952681
7 changed files with 111 additions and 51 deletions
|
|
@ -114,10 +114,10 @@ defmodule Towerops.Agents do
|
|||
{:error, :invalid_token}
|
||||
|
||||
"""
|
||||
def verify_agent_token(encoded_token) do
|
||||
with {:ok, token_hash} <- AgentToken.verify_token(encoded_token),
|
||||
def verify_agent_token(token) do
|
||||
with {:ok, token_string} <- AgentToken.verify_token(token),
|
||||
%AgentToken{} = agent_token <-
|
||||
Repo.get_by(AgentToken, token_hash: token_hash, enabled: true) do
|
||||
Repo.get_by(AgentToken, token: token_string, enabled: true) do
|
||||
{:ok, agent_token}
|
||||
else
|
||||
_ -> {:error, :invalid_token}
|
||||
|
|
|
|||
|
|
@ -3,20 +3,20 @@ defmodule Towerops.Agents.AgentToken do
|
|||
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, and only the SHA256 hash is
|
||||
stored in the database for security.
|
||||
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
|
||||
|
||||
@hash_algorithm :sha256
|
||||
@rand_size 32
|
||||
# 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_hash, :binary
|
||||
field :token, :string
|
||||
field :name, :string
|
||||
field :last_seen_at, :utc_datetime
|
||||
field :last_ip, :string
|
||||
|
|
@ -29,22 +29,24 @@ defmodule Towerops.Agents.AgentToken do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Builds a new agent token with secure random bytes.
|
||||
Builds a new agent token with cryptographically secure random bytes.
|
||||
|
||||
Returns a tuple of {encoded_token, changeset}. The encoded_token should be
|
||||
returned to the user ONCE and never stored in plaintext.
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
"""
|
||||
def build_token(organization_id, name) do
|
||||
# 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)
|
||||
hashed_token = :crypto.hash(@hash_algorithm, token)
|
||||
|
||||
changeset =
|
||||
%__MODULE__{}
|
||||
|
|
@ -52,38 +54,33 @@ defmodule Towerops.Agents.AgentToken do
|
|||
%{
|
||||
organization_id: organization_id,
|
||||
name: name,
|
||||
token_hash: hashed_token
|
||||
token: encoded_token
|
||||
},
|
||||
[:organization_id, :name, :token_hash]
|
||||
[:organization_id, :name, :token]
|
||||
)
|
||||
|> validate_required([:organization_id, :name, :token_hash])
|
||||
|> validate_required([:organization_id, :name, :token])
|
||||
|> unique_constraint(:token)
|
||||
|
||||
{encoded_token, changeset}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Verifies an encoded token by hashing it.
|
||||
Verifies a token by checking if it exists in the database.
|
||||
|
||||
Returns {:ok, token_hash} if the token can be decoded and hashed,
|
||||
otherwise {:error, :invalid_token}.
|
||||
Returns {:ok, token} if valid, otherwise {:error, :invalid_token}.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> AgentToken.verify_token("valid_base64_token")
|
||||
{:ok, <<hashed_bytes>>}
|
||||
iex> AgentToken.verify_token("valid_token")
|
||||
{:ok, "valid_token"}
|
||||
|
||||
iex> AgentToken.verify_token("invalid")
|
||||
{:error, :invalid_token}
|
||||
|
||||
"""
|
||||
def verify_token(encoded_token) do
|
||||
case Base.url_decode64(encoded_token, padding: false) do
|
||||
{:ok, token} ->
|
||||
hashed = :crypto.hash(@hash_algorithm, token)
|
||||
{:ok, hashed}
|
||||
|
||||
_ ->
|
||||
{:error, :invalid_token}
|
||||
end
|
||||
def verify_token(token) when is_binary(token) do
|
||||
{:ok, token}
|
||||
end
|
||||
|
||||
def verify_token(_), do: {:error, :invalid_token}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -58,6 +58,44 @@ defmodule ToweropsWeb.AgentLive.Index do
|
|||
|> assign(:new_token, nil)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("show_setup", %{"id" => id}, socket) do
|
||||
agent_token = Agents.get_agent_token!(id)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:new_token, %{agent_token: agent_token, token: agent_token.token})
|
||||
|> assign(:show_token_modal, true)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("regenerate_token", %{"id" => id}, socket) do
|
||||
agent_token = Agents.get_agent_token!(id)
|
||||
organization = socket.assigns.current_organization
|
||||
|
||||
# Revoke the old token and create a new one
|
||||
case Agents.revoke_agent_token(id) do
|
||||
{:ok, _} ->
|
||||
case Agents.create_agent_token(organization.id, agent_token.name) do
|
||||
{:ok, new_agent_token, token} ->
|
||||
agent_tokens = Agents.list_organization_agent_tokens(organization.id)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:agent_tokens, agent_tokens)
|
||||
|> assign(:new_token, %{agent_token: new_agent_token, token: token})
|
||||
|> assign(:show_token_modal, true)
|
||||
|> put_flash(:info, "New token generated successfully")}
|
||||
|
||||
{:error, _changeset} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to generate new token")}
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to revoke old token")}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("revoke_agent", %{"id" => id}, socket) do
|
||||
case Agents.revoke_agent_token(id) do
|
||||
|
|
|
|||
|
|
@ -106,15 +106,25 @@
|
|||
|
||||
<:action :let={agent}>
|
||||
<%= if agent.enabled do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="revoke_agent"
|
||||
phx-value-id={agent.id}
|
||||
data-confirm="Are you sure you want to revoke this agent? It will no longer be able to authenticate."
|
||||
class="text-sm font-medium text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
phx-click="show_setup"
|
||||
phx-value-id={agent.id}
|
||||
class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
View Setup
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="revoke_agent"
|
||||
phx-value-id={agent.id}
|
||||
data-confirm="Are you sure you want to revoke this agent? It will no longer be able to authenticate."
|
||||
class="text-sm font-medium text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
</div>
|
||||
<% else %>
|
||||
<span class="text-xs text-zinc-500 dark:text-zinc-400">Revoked</span>
|
||||
<% end %>
|
||||
|
|
@ -128,21 +138,23 @@
|
|||
<div
|
||||
id="token-modal"
|
||||
class="fixed inset-0 z-50 overflow-y-auto"
|
||||
phx-click="close_token_modal"
|
||||
phx-mounted={JS.show()}
|
||||
>
|
||||
<div class="flex min-h-screen items-center justify-center p-4">
|
||||
<div class="fixed inset-0 bg-zinc-500/75 dark:bg-zinc-950/75 transition-opacity"></div>
|
||||
<div
|
||||
class="fixed inset-0 bg-zinc-500/75 dark:bg-zinc-950/75 transition-opacity"
|
||||
phx-click="close_token_modal"
|
||||
></div>
|
||||
|
||||
<div class="relative bg-white dark:bg-zinc-900 rounded-lg shadow-xl max-w-2xl w-full">
|
||||
<div class="p-6">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
Agent Created Successfully
|
||||
Agent Setup Instructions
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Copy this token now - it will never be shown again!
|
||||
Use the token and Docker Compose configuration below to deploy this agent.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -193,10 +205,9 @@
|
|||
<h4 class="text-sm font-medium text-zinc-900 dark:text-zinc-100 mb-2">
|
||||
Docker Compose Example
|
||||
</h4>
|
||||
<pre class="text-xs font-mono text-zinc-600 dark:text-zinc-400 overflow-x-auto"><code>version: '3.8'
|
||||
services:
|
||||
<pre class="text-xs font-mono text-zinc-600 dark:text-zinc-400 overflow-x-auto"><code>services:
|
||||
towerops-agent:
|
||||
image: towerops/agent:latest
|
||||
image: gmcintire/towerops-agent:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TOWEROPS_API_URL={url(@socket, ~p"/")}
|
||||
|
|
@ -210,10 +221,9 @@ services:
|
|||
>
|
||||
Copy to clipboard
|
||||
</button>
|
||||
<textarea id="docker-compose-example" class="sr-only">version: '3.8'
|
||||
services:
|
||||
<textarea id="docker-compose-example" class="sr-only">services:
|
||||
towerops-agent:
|
||||
image: towerops/agent:latest
|
||||
image: gmcintire/towerops-agent:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TOWEROPS_API_URL={url(@socket, ~p"/")}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
defmodule Towerops.Repo.Migrations.ChangeAgentTokenToPlaintext do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
# Drop existing tokens since we can't migrate hashed tokens to plaintext
|
||||
execute "DELETE FROM agent_tokens", "DELETE FROM agent_tokens"
|
||||
|
||||
# Rename column and change type
|
||||
alter table(:agent_tokens) do
|
||||
remove :token_hash
|
||||
add :token, :text, null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -24,7 +24,8 @@ defmodule Towerops.AgentsTest do
|
|||
assert agent_token.organization_id == org.id
|
||||
assert agent_token.name == "Test Agent"
|
||||
assert agent_token.enabled == true
|
||||
assert is_binary(agent_token.token_hash)
|
||||
assert is_binary(agent_token.token)
|
||||
assert agent_token.token == token
|
||||
end
|
||||
|
||||
test "returns error with invalid data" do
|
||||
|
|
|
|||
|
|
@ -39,9 +39,9 @@ defmodule ToweropsWeb.AgentLiveTest do
|
|||
|> form("#new-agent-form form", %{name: "Test Agent"})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "Agent Created Successfully"
|
||||
assert html =~ "Agent Setup Instructions"
|
||||
assert html =~ "Test Agent"
|
||||
assert html =~ "Copy this token now"
|
||||
assert html =~ "Authentication Token"
|
||||
end
|
||||
|
||||
test "revokes agent", %{conn: conn, organization: organization} do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue