defmodule ExSecrets do @moduledoc """ This module functions to access secrets in an Elixir application. """ alias ExSecrets.Cache alias ExSecrets.Utils.Resolver alias ExSecrets.Utils.SecretFetchLimiter require Logger @doc """ Get secret value. You can pass two options: - provider: Name of the provider to use. Default is :system_env - default_value: Default value to return if secret is not found. Default is nil ## Examples iex> ExSecrets.get("FOO") nil iex> Application.put_env(:ex_secrets, :default_provider, :dot_env) :ok iex> ExSecrets.get("FOO") nil iex> System.put_env "FOO", "BAR" :ok iex> ExSecrets.get("FOO") "BAR" iex> System.delete_env "FOO" :ok iex> ExSecrets.get("FOO") "BAR" iex> ExSecrets.reset() :ok iex> ExSecrets.get("FOO") nil iex> Application.delete_env(:ex_secrets, :default_provider) :ok iex> Application.put_env(:ex_secrets, :providers, %{dot_env: %{path: "test/support/fixtures/dot_env_test.env"}}) :ok iex> ExSecrets.get("ERL", provider: :dot_env) nil iex> ExSecrets.get("ERL", provider: :dot_env, default_value: "ANG") "ANG" iex> Application.delete_env(:ex_secrets, :providers) :ok iex> Application.put_env(:ex_secrets, :providers, %{dot_env: %{path: "test/support/fixtures/dot_env_test.env"}}) :ok iex> ExSecrets.get("DEVS", provider: :dot_env) "ROCKS" iex> Application.delete_env(:ex_secrets, :providers) :ok """ @spec get(String.t(), provider: atom(), default_value: any()) :: String.t() | nil def get(key, opts \\ []) def get(key, []) do with provider when is_atom(provider) <- get_any_provider(), value when not is_nil(value) <- get(key, provider: provider) do value else _ -> get_default(key) end end def get(key, provider: provider, default_value: default_value) do with value when is_nil(value) <- Cache.get(key), fetch <- SecretFetchLimiter.allow(key, ExSecrets, :get_using_provider, [key, provider]), value when not is_nil(value) <- Cache.pass_by(key, fetch) do value else nil -> default_value value -> value end end def get(key, provider: provider) do with value when is_nil(value) <- Cache.get(key), fetch <- SecretFetchLimiter.allow(key, ExSecrets, :get_using_provider, [key, provider]), value when not is_nil(value) <- Cache.pass_by(key, fetch) do value else nil -> get_default(key) value -> value end end def get(key, default_value: default_value) do with value when is_nil(value) <- Cache.get(key), provider <- get_any_provider(), fetch <- SecretFetchLimiter.allow(key, ExSecrets, :get_using_provider, [key, provider]), value when not is_nil(value) <- Cache.pass_by(key, fetch) do value else nil -> default_value value -> value end end def get(key, provider) do m = "ExSecrets.get(key, provider) is deprecated. Use ExSecrets.get/2 with options." IO.puts("#{IO.ANSI.yellow()}ExSecrets Warning ==> #{NaiveDateTime.utc_now()} ==> #{m}") get(key, provider: provider) end @doc """ Get secret value with provider name and default value """ @deprecated "Use get/2 instead." def get(key, provider, default) do case get(key, provider: provider) do nil -> default value -> value end end @doc """ Set secret value. Supported for providers: - :system_env - :dot_env - :azure_key_vault - :azure_managed_identity - :google_secret_manager Calling this function requires the provider to be configured with credentials that allow create secrets like Secret Admionistrator in Azure Key Vault. ## Examples iex> File.touch(".env.dev") :ok iex> Application.put_env(:ex_secrets, :providers, %{dot_env: %{path: ".env.dev"}}) :ok iex> ExSecrets.set("TEST", "test", provider: :dot_env) :ok iex> Application.delete_env(:ex_secrets, :providers) :ok iex> File.rm(".env.dev") """ @spec set(String.t(), String.t(), Keyword.t()) :: :ok | :error def set(key, value, opts \\ []) def set(key, value, opts) do with provider <- Keyword.get(opts, :provider), provider when is_atom(provider) <- Resolver.call(provider), :ok <- Kernel.apply(provider, :set, [key, value]) do Cache.save(key, value) :ok else _ -> :error end end @doc """ Internal function for fetching secret with provide for catching and rate limiting. Do not rely on this function. """ def get_using_provider(key, provider) do with provider when is_atom(provider) <- Resolver.call(provider), value <- Kernel.apply(provider, :get, [key]) do value else _ -> get_default(key) end end defp get_default(key) do with value when is_nil(value) <- Cache.get(key), provider when provider not in [:system_env] <- get_default_prider(), fetch <- SecretFetchLimiter.allow(key, ExSecrets, :get_using_provider, [key, provider]) do Cache.pass_by(key, fetch) else provider when is_atom(provider) -> get_using_provider(key, provider) value -> value end end defp get_default_prider() do Application.get_env(:ex_secrets, :default_provider, :system_env) end defp get_any_provider() do with providers when is_map(providers) <- Application.get_env(:ex_secrets, :providers, %{}), provider <- Map.keys(providers) |> Kernel.++([:system_env]) |> Enum.at(0) do provider else _ -> nil end end @doc """ Resets cache and reloads all providers. """ def reset() do GenServer.call(:ex_secrets_cache_store, :clear) ExSecrets.Application.get_providers() |> Enum.each(&Kernel.apply(&1, :reset, [])) :ok end end