Skip to content

Commit

Permalink
adds get with options
Browse files Browse the repository at this point in the history
  • Loading branch information
zemuldo committed Oct 28, 2023
1 parent 807de8f commit a91bd62
Show file tree
Hide file tree
Showing 21 changed files with 252 additions and 89 deletions.
34 changes: 34 additions & 0 deletions .check.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[
## don't run tools concurrently
# parallel: false,

## don't print info about skipped tools
# skipped: false,

## always run tools in fix mode (put it in ~/.check.exs locally, not in project config)
# fix: true,

## don't retry automatically even if last run resulted in failures
# retry: false,

## list of tools (see `mix check` docs for a list of default curated tools)
tools: [
## curated tools may be disabled (e.g. the check for compilation warnings)
# {:compiler, false},

## ...or have command & args adjusted (e.g. enable skip comments for sobelow)
{:sobelow, "mix sobelow --exit --skip -i Config.HTTPS,Config.Headers --threshold medium"},

## ...or reordered (e.g. to see output from dialyzer before others)
# {:dialyzer, order: -1},
{:dialyzer, false},

## ...or reconfigured (e.g. disable parallel execution of ex_unit in umbrella)
{:ex_unit, false},

## custom new tools may be added (Mix tasks or arbitrary commands)
# {:my_task, "mix my_task", env: %{"MIX_ENV" => "prod"}},
# {:my_tool, ["my_tool", "arg with spaces"]}
{:doctor, false}
]
]
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
elixir-version: ${{ env.ELIXIR_VERSION }}
- run: echo 'JAVA=SCRIPT\nER=LANG' > .env
- run: mix deps.get
- run: mix check
- run: mix test

- name: Elixir Hex, mix build Cache
Expand Down
19 changes: 19 additions & 0 deletions config/.credo.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
%{
configs: [
%{
name: "default",
files: %{
included: ["lib/"],
excluded: []
},
plugins: [],
requires: [],
strict: false,
parse_timeout: 5000,
color: true,
checks: [
{Credo.Check.Readability.WithSingleClause, false}
]
}
]
}
3 changes: 3 additions & 0 deletions lib/cache.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
defmodule ExSecrets.Cache do
@moduledoc """
Cache module provides helper functions for caching secrets.
"""
use GenServer

@store_name :ex_secrets_cache_store
Expand Down
139 changes: 97 additions & 42 deletions lib/ex_secrets.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,107 @@ defmodule ExSecrets do
alias ExSecrets.Utils.Resolver
alias ExSecrets.Utils.SecretFetchLimiter

require Logger

@doc """
Get secret value
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(1)> ExSecrets.get("FOO")
iex> ExSecrets.get("FOO")
nil
iex(2)> Application.put_env(:ex_secrets, :default_provider, :dot_env)
iex> Application.put_env(:ex_secrets, :default_provider, :dot_env)
:ok
iex(3)> ExSecrets.get("FOO")
iex> ExSecrets.get("FOO")
nil
iex(4)> System.put_env "FOO", "BAR"
iex> System.put_env "FOO", "BAR"
:ok
iex> ExSecrets.get("FOO")
"BAR"
iex> System.delete_env "FOO"
:ok
iex(5)> ExSecrets.get("FOO")
iex> ExSecrets.get("FOO")
"BAR"
iex(6)> System.delete_env "FOO"
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(7)> Application.delete_env(:ex_secrets, :default_provider)
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
"""
def get(key) do
case get_any_provider() do
provider when is_atom(provider) -> get(key, provider)
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

@doc """
Get secret value with provider name.
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

## Examples
iex(1)> Application.put_env(:ex_secrets, :providers, %{dot_env: %{path: "test/support/fixtures/dot_env_test.env"}})
:ok
iex(2)> ExSecrets.get("DEVS", :dot_env)
"ROCKS"
iex(4)> Application.delete_env(:ex_secrets, :providers)
:ok
"""
def get(key, provider) do
with value when not is_nil(value) <- Cache.get(key) do
m = "ExSecrets.get(key, provider) is deprecated. Use ExSecrets.get/2 with options."

cond do
has_fun?(Logger, :warn) -> Logger.warn(m)
has_fun?(Logger, :warning) -> Logger.warning(m)
true -> :ok
end

with true <- is_atom(provider),
value when not is_nil(value) <- Cache.get(key) do
value
else
nil ->
Expand All @@ -59,19 +120,11 @@ defmodule ExSecrets do

@doc """
Get secret value with provider name and default value
## Examples
iex(1)> Application.put_env(:ex_secrets, :providers, %{dot_env: %{path: "test/support/fixtures/dot_env_test.env"}})
:ok
iex(2)> ExSecrets.get("ERL", :dot_env)
nil
iex(3)> ExSecrets.get("ERL", :dot_env, "ANG")
"ANG"
iex(4)> Application.delete_env(:ex_secrets, :providers)
:ok
"""
@deprecated "This function is deprecated. Use get/2 instead."
def get(key, provider, default) do
with value when not is_nil(value) <- Cache.get(key) do
with true <- is_atom(provider),
value when not is_nil(value) <- Cache.get(key) do
value
else
nil ->
Expand All @@ -85,7 +138,6 @@ defmodule ExSecrets do
end
end


@doc """
Internal function for fetching secret with provide for catching and rate limiting.
Do not rely on this function.
Expand All @@ -101,12 +153,11 @@ defmodule ExSecrets do

defp get_default(key) do
with value when is_nil(value) <- Cache.get(key),
provider <- get_default_prider() do
Cache.pass_by(
key,
SecretFetchLimiter.allow(key, ExSecrets, :get_using_provider, [key, provider])
)
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
Expand All @@ -125,12 +176,16 @@ defmodule ExSecrets do
end
end

@doc """
@doc """
Resets cache and reloads all providers.
"""
def reset() do
n = GenServer.call(:ex_secrets_cache_store, :clear)
GenServer.call(:ex_secrets_cache_store, :clear)
ExSecrets.Application.get_providers() |> Enum.each(&Kernel.apply(&1, :reset, []))
{:ok, n}
:ok
end

defp has_fun?(module, func) do
Keyword.has_key?(module.__info__(:functions), func)
end
end
3 changes: 3 additions & 0 deletions lib/http_adapter_behavior.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
defmodule ExSecrets.HTTPAdapterBehavior do
@moduledoc """
Behaviour for HTTP adapters.
"""
@callback get(binary(), map()) :: {:ok, map()} | {:error, binary()}
@callback post(binary(), map(), map()) :: {:ok, map()} | {:error, binary()}
end
7 changes: 6 additions & 1 deletion lib/providers/azure_key_vault.ex
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ defmodule ExSecrets.Providers.AzureKeyVault do
end
end

def set(_name, _value) do
{:error, "Not implemented"}
end

def handle_call({:get, name}, _from, state) do
case get_secret(name, state, get_current_epoch()) do
{:ok, secret, state} -> {:reply, secret, state}
Expand All @@ -131,7 +135,8 @@ defmodule ExSecrets.Providers.AzureKeyVault do
current_time
)
when issued_at + expires_in - current_time > 5 do
with {:ok, value} <- get_secret_call(name, access_token) do
with {:ok, value} <- get_secret_call(name, access_token),
true <- is_binary(value) do
{:ok, value, state}
else
_ -> {:error, "Failed to get secret"}
Expand Down
7 changes: 6 additions & 1 deletion lib/providers/azure_managed_identity.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ defmodule ExSecrets.Providers.AzureManagedIdentity do
end
end

def set(_name, _value) do
{:error, "Not implemented"}
end

def handle_call({:get, name}, _from, state) do
case get_secret(name, state, get_current_epoch()) do
{:ok, secret, state} -> {:reply, secret, state}
Expand All @@ -69,7 +73,8 @@ defmodule ExSecrets.Providers.AzureManagedIdentity do
current_time
)
when issued_at + expires_in - current_time > 5 do
with {:ok, value} <- get_secret_call(name, access_token) do
with {:ok, value} <- get_secret_call(name, access_token),
true <- is_binary(value) do
{:ok, value, state}
else
_ -> {:error, "Failed to get secret"}
Expand Down
2 changes: 2 additions & 0 deletions lib/providers/behaviour.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ defmodule ExSecrets.Providers.Behaviour do
Gets a secret from the provider.
"""
@callback get(String.t()) :: String.t()

@callback set(String.t(), String.t()) :: {:ok, String.t()} | {:error, term()}
end
4 changes: 4 additions & 0 deletions lib/providers/dot_env.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ defmodule ExSecrets.Providers.DotEnv do
end
end

def set(_name, _value) do
{:error, "Not implemented"}
end

def init(_) do
read_env()
{:ok, %{}}
Expand Down
7 changes: 6 additions & 1 deletion lib/providers/google_secret_manager.ex
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ defmodule ExSecrets.Providers.GoogleSecretManager do
end
end

def set(_name, _value) do
{:error, "Not implemented"}
end

def handle_call({:get, name}, _from, state) do
case get_secret(name, state, get_current_epoch()) do
{:ok, secret, state} -> {:reply, secret, state}
Expand All @@ -88,7 +92,8 @@ defmodule ExSecrets.Providers.GoogleSecretManager do
current_time
)
when issued_at + expires_in - current_time > 5 do
with {:ok, value} <- get_secret_call(name, access_token, state.cred) do
with {:ok, value} <- get_secret_call(name, access_token, state.cred),
true <- is_binary(value) do
{:ok, value, state}
else
_ -> {:error, "Failed to get secret"}
Expand Down
8 changes: 8 additions & 0 deletions lib/providers/system_env.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
defmodule ExSecrets.Providers.SystemEnv do
@moduledoc """
SystemEnv provider provides secrets from the system environment.
"""
use ExSecrets.Providers.Base

def init(_) do
Expand All @@ -13,6 +16,11 @@ defmodule ExSecrets.Providers.SystemEnv do
System.get_env(name)
end

def set(name, value) do
System.put_env(name, value)
{:ok, value}
end

def process_name() do
:ex_secrets_system_env
end
Expand Down
3 changes: 3 additions & 0 deletions lib/utils/config.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
defmodule ExSecrets.Utils.Config do
@moduledoc """
Config module provides helper functions for getting configuration values.
"""
def provider_config_value(provider, key) do
provider
|> provider_env()
Expand Down
3 changes: 3 additions & 0 deletions lib/utils/resolver.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
defmodule ExSecrets.Utils.Resolver do
@moduledoc """
Resolver module resolves the provider name to the actual provider module.
"""
def call(:system_env), do: ExSecrets.Providers.SystemEnv
def call(:dot_env), do: ExSecrets.Providers.DotEnv
def call(:azure_managed_identity), do: ExSecrets.Providers.AzureManagedIdentity
Expand Down
Loading

0 comments on commit a91bd62

Please sign in to comment.