107 lines
2.7 KiB
Elixir
107 lines
2.7 KiB
Elixir
defmodule Plausible.Plugins.API.Token do
|
|
@moduledoc """
|
|
Ecto schema for Plugins API Tokens.
|
|
Tokens are stored hashed and require a description.
|
|
|
|
Tokens are considered secret, although the Plugins API
|
|
by nature will expose very little, if any, destructive/insecure operations.
|
|
|
|
The raw token version is meant to be presented to the user upon creation.
|
|
It is prefixed with a plain text identifier allowing source scanning
|
|
for leaked secrets.
|
|
"""
|
|
use Plausible
|
|
use Ecto.Schema
|
|
import Ecto.Changeset
|
|
|
|
alias Plausible.Site
|
|
|
|
@type t() :: %__MODULE__{}
|
|
|
|
@primary_key {:id, :binary_id, autogenerate: true}
|
|
schema "plugins_api_tokens" do
|
|
timestamps()
|
|
field(:token_hash, :binary)
|
|
field(:description, :string)
|
|
field(:hint, :string)
|
|
field(:last_used_at, :naive_datetime)
|
|
|
|
belongs_to(:site, Site)
|
|
end
|
|
|
|
@spec generate(String.t()) :: map()
|
|
def generate(random_bytes \\ random_bytes()) do
|
|
raw = prefixed(random_bytes)
|
|
hash = hash(raw)
|
|
|
|
%{
|
|
raw: raw,
|
|
hash: hash
|
|
}
|
|
end
|
|
|
|
@spec hash(String.t()) :: binary()
|
|
def hash(raw) do
|
|
:crypto.hash(:sha256, raw)
|
|
end
|
|
|
|
@fields [:description, :site_id]
|
|
@required_fields [:description, :site, :token_hash, :hint]
|
|
|
|
@spec insert_changeset(Site.t(), map(), map()) :: Ecto.Changeset.t()
|
|
def insert_changeset(site, %{hash: hash, raw: raw}, attrs \\ %{}) do
|
|
%__MODULE__{}
|
|
|> cast(attrs, @fields)
|
|
|> put_change(:token_hash, hash)
|
|
|> put_change(:hint, String.slice(raw, -4, 4))
|
|
|> put_assoc(:site, site)
|
|
|> validate_required(@required_fields)
|
|
end
|
|
|
|
@doc """
|
|
Raw tokens are prefixed so that tools like
|
|
https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
|
can scan repositories for accidental secret commits.
|
|
"""
|
|
def prefix() do
|
|
on_ee do
|
|
env = Application.get_env(:plausible, :environment)
|
|
|
|
case env do
|
|
"prod" -> "plausible-plugin"
|
|
env -> "plausible-plugin-#{env}"
|
|
end
|
|
else
|
|
"plausible-plugin-selfhost"
|
|
end
|
|
end
|
|
|
|
@spec last_used_humanize(t()) :: String.t()
|
|
def last_used_humanize(token) do
|
|
diff =
|
|
if token.last_used_at do
|
|
now = NaiveDateTime.utc_now()
|
|
NaiveDateTime.diff(now, token.last_used_at, :minute)
|
|
end
|
|
|
|
cond do
|
|
is_nil(diff) -> "Not yet"
|
|
diff < 5 -> "Just recently"
|
|
diff < 30 -> "Several minutes ago"
|
|
diff < 70 -> "An hour ago"
|
|
diff < 24 * 60 -> "Hours ago"
|
|
diff < 24 * 60 * 2 -> "Yesterday"
|
|
diff < 24 * 60 * 7 -> "Sometime this week"
|
|
true -> "Long time ago"
|
|
end
|
|
end
|
|
|
|
defp prefixed(random_bytes) do
|
|
Enum.join([prefix(), random_bytes], "-")
|
|
end
|
|
|
|
defp random_bytes() do
|
|
30 |> :crypto.strong_rand_bytes() |> Base.encode64()
|
|
end
|
|
end
|