Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
96 lines
2.4 KiB
Elixir
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
|