defmodule Plausible.Auth.ApiKey do @moduledoc """ There are two kinds of API keys, legacy API keys and team-scoped API keys. Legacy keys have `team` / `team_id` set to `nil`. Legacy Stats API keys can be used to - access the stats of sites of any team of the user, - access the stats of sites that they are a guest of, - access data about the sites of the teams that they belong to, - access data about sites that they are a guest of. Legacy Sites API keys allow the above and additionally - to provision sites for any team of the user, - to configure sites for any team of the user, - to configure sites that they are a guest of. It's not possible to create legacy keys any more through the UI. Team-scoped keys have `team` / `team_id` set to a team. Team-scoped Stats API keys can be used to - access the stats of sites of that team, - access data about the sites of that team. Team-scoped Sites API keys allow the above and additionally - to provision sites for that team, - to configure sites for that team. Only team members can use team-scoped keys. """ use Plausible use Ecto.Schema import Ecto.Changeset @type t() :: %__MODULE__{} @required [:user_id, :name] @optional [:key, :scopes] @default_hourly_request_limit_per_team on_ee(do: 600, else: 1_000_000) schema "api_keys" do field :name, :string field :scopes, {:array, :string}, default: ["stats:read:*"] field :type, :string, virtual: true field :key, :string, virtual: true field :key_hash, :string field :key_prefix, :string belongs_to :team, Plausible.Teams.Team belongs_to :user, Plausible.Auth.User timestamps() end defp config(), do: Application.fetch_env!(:plausible, __MODULE__) def default_hourly_request_limit(), do: @default_hourly_request_limit_per_team def limit_key(team), do: "api_request:team:#{team.identifier}" def legacy_hourly_request_limit() do config() |> Keyword.fetch!(:legacy_per_user_hourly_request_limit) end def legacy_limit_key(user), do: "api_request:legacy_user:#{user.id}" def burst_request_limit(), do: config() |> Keyword.fetch!(:burst_request_limit) def burst_period_seconds(), do: config() |> Keyword.fetch!(:burst_period_seconds) def changeset(struct, team, attrs) when not is_nil(team) do struct |> cast(attrs, @required ++ @optional) |> validate_required(@required) |> maybe_put_key() |> process_key() |> put_assoc(:team, team) |> unique_constraint(:key_hash, error_key: :key) |> unique_constraint([:team_id, :user_id], error_key: :team) end def do_hash(key) do :crypto.hash(:sha256, [secret_key_base(), key]) |> Base.encode16() |> String.downcase() end def process_key(%{errors: [], changes: changes} = changeset) do prefix = binary_part(changes[:key], 0, 6) change(changeset, key_hash: do_hash(changes[:key]), key_prefix: prefix ) end def process_key(changeset), do: changeset defp maybe_put_key(changeset) do if get_change(changeset, :key) do changeset else key = :crypto.strong_rand_bytes(64) |> Base.url_encode64() |> binary_part(0, 64) put_change(changeset, :key, key) end end defp secret_key_base() do Application.get_env(:plausible, PlausibleWeb.Endpoint) |> Keyword.fetch!(:secret_key_base) end end