tarakan/lib/tarakan/accounts/ssh_keys.ex
Maxfield Luke af6077b9c3
All checks were successful
CI and deploy / Test (push) Successful in 4m52s
CI and deploy / Deploy production (push) Successful in 23s
Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at
https://git.elektrine.com/elektrine/tarakan.
2026-07-29 04:43:40 -04:00

96 lines
2.4 KiB
Elixir

defmodule Tarakan.Accounts.SshKeys do
@moduledoc """
Managing and resolving registered SSH public keys.
"""
import Ecto.Query, warn: false
alias Tarakan.Accounts.{Account, SshKey}
alias Tarakan.Repo
@maximum_keys_per_account 10
def list_for_account(%Account{id: account_id}) do
SshKey
|> where([key], key.account_id == ^account_id)
|> order_by([key], asc: key.inserted_at)
|> Repo.all()
end
@doc """
Registers a pasted OpenSSH public key for an account.
Requires a confirmed email address: refuses `{:error, :unconfirmed_email}`
until the owner has proved mailbox control with a magic-link login.
"""
def add_key(%Account{} = account, attrs) do
Repo.transaction(fn ->
# Serialize on the account row so concurrent adds cannot blow the cap.
locked =
Repo.one!(
from candidate in Account, where: candidate.id == ^account.id, lock: "FOR UPDATE"
)
if is_nil(locked.confirmed_at) do
Repo.rollback(:unconfirmed_email)
end
count =
SshKey
|> where([key], key.account_id == ^account.id)
|> Repo.aggregate(:count)
if count >= @maximum_keys_per_account do
Repo.rollback(:key_limit)
end
%SshKey{account_id: account.id}
|> SshKey.changeset(attrs)
|> Repo.insert()
|> case do
{:ok, key} -> key
{:error, changeset} -> Repo.rollback(changeset)
end
end)
end
@doc "Removes one of the account's own keys."
def delete_key(%Account{id: account_id}, key_id) do
case Repo.get_by(SshKey, id: key_id, account_id: account_id) do
%SshKey{} = key ->
Repo.delete(key)
nil ->
{:error, :not_found}
end
end
@doc """
Resolves a decoded public key (as presented during SSH auth) to its account.
The global fingerprint uniqueness makes this a pure lookup.
"""
def find_account_by_key(public_key) do
fingerprint = SshKey.fingerprint(public_key)
query =
from key in SshKey,
where: key.fingerprint_sha256 == ^fingerprint,
join: account in assoc(key, :account),
preload: [account: account]
case Repo.one(query) do
%SshKey{} = key -> {:ok, key.account, key}
nil -> :error
end
rescue
_error -> :error
end
def touch_last_used(%SshKey{id: id}) do
from(key in SshKey, where: key.id == ^id)
|> Repo.update_all(set: [last_used_at: DateTime.utc_now(:microsecond)])
:ok
end
end