From 411f1f0e4e2d2d3e6b464a460a3d33b7e31f0c1c Mon Sep 17 00:00:00 2001 From: Zemuldo Date: Sun, 29 Oct 2023 09:59:37 +0300 Subject: [PATCH] adds set secret --- lib/ex_secrets.ex | 22 ++++-- lib/providers/azure_key_vault.ex | 10 +-- lib/providers/azure_managed_identity.ex | 10 +-- lib/providers/google_secret_manager.ex | 95 ++++++++++++++++++++++++- 4 files changed, 121 insertions(+), 16 deletions(-) diff --git a/lib/ex_secrets.ex b/lib/ex_secrets.ex index 2f73d1b..356af42 100644 --- a/lib/ex_secrets.ex +++ b/lib/ex_secrets.ex @@ -52,6 +52,7 @@ defmodule ExSecrets do 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 @@ -120,13 +121,26 @@ defmodule ExSecrets do end end + @doc """ + Set secret value. + + Supported for providers: + + - :system_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. + """ + + @spec set(Atom.t(), String.t(), String.t()) :: :ok | :error def set(provider, key, value) do - with provider when is_atom(provider) <- Resolver.call(provider) do - Kernel.apply(provider, :set, [key, value]) + with provider when is_atom(provider) <- Resolver.call(provider), + :ok <- Kernel.apply(provider, :set, [key, value]) do Cache.save(key, value) else - {:error, message} -> {:error, message} - _ -> {:error, :provider_not_found} + _ -> :error end end diff --git a/lib/providers/azure_key_vault.ex b/lib/providers/azure_key_vault.ex index ed95a0c..46bac62 100644 --- a/lib/providers/azure_key_vault.ex +++ b/lib/providers/azure_key_vault.ex @@ -122,11 +122,11 @@ defmodule ExSecrets.Providers.AzureKeyVault do else nil -> case set_secret(name, value, %{}, nil) do - {:ok, value, _} -> - value + {:ok, _value, _} -> + :ok _ -> - {:error, "Could not set secret, check credentials and permissions"} + :error end end end @@ -140,8 +140,8 @@ defmodule ExSecrets.Providers.AzureKeyVault do def handle_call({:set, name, value}, _from, state) do case set_secret(name, value, state, get_current_epoch()) do - {:ok, secret, state} -> {:reply, secret, state} - _ -> {:reply, nil, state} + {:ok, _secret, state} -> {:reply, :ok, state} + _ -> {:reply, :error, state} end end diff --git a/lib/providers/azure_managed_identity.ex b/lib/providers/azure_managed_identity.ex index fc72920..31bfdf6 100644 --- a/lib/providers/azure_managed_identity.ex +++ b/lib/providers/azure_managed_identity.ex @@ -59,11 +59,11 @@ defmodule ExSecrets.Providers.AzureManagedIdentity do else nil -> case set_secret(name, value, %{}, nil) do - {:ok, value, _} -> - value + {:ok, _value, _} -> + :ok _ -> - {:error, "Could not set secret, check credentials and permissions"} + :error end end end @@ -77,8 +77,8 @@ defmodule ExSecrets.Providers.AzureManagedIdentity do def handle_call({:set, name, value}, _from, state) do case set_secret(name, value, state, get_current_epoch()) do - {:ok, secret, state} -> {:reply, secret, state} - _ -> {:reply, nil, state} + {:ok, _secret, state} -> {:reply, :ok, state} + _ -> {:reply, :error, state} end end diff --git a/lib/providers/google_secret_manager.ex b/lib/providers/google_secret_manager.ex index c99ac25..15d9937 100644 --- a/lib/providers/google_secret_manager.ex +++ b/lib/providers/google_secret_manager.ex @@ -74,8 +74,22 @@ defmodule ExSecrets.Providers.GoogleSecretManager do end end - def set(_name, _value) do - {:error, "Not implemented"} + def set(name, value) do + name = name |> String.split("_") |> Enum.join("-") + + with process when not is_nil(process) <- + GenServer.whereis(@process_name) do + GenServer.call(@process_name, {:set, name, value}) + else + nil -> + case set_secret(name, value, %{}, nil) do + {:ok, _value, _} -> + :ok + + _ -> + :error + end + end end def handle_call({:get, name}, _from, state) do @@ -85,6 +99,13 @@ defmodule ExSecrets.Providers.GoogleSecretManager do end end + def handle_call({:set, name, value}, _from, state) do + case set_secret(name, value, state, get_current_epoch()) do + {:ok, _secret, state} -> {:reply, :ok, state} + _ -> {:reply, :error, state} + end + end + defp get_secret( name, %{"access_token" => access_token, "issued_at" => issued_at, "expires_in" => expires_in} = @@ -110,6 +131,32 @@ defmodule ExSecrets.Providers.GoogleSecretManager do end end + defp set_secret( + name, + value, + %{"access_token" => access_token, "issued_at" => issued_at, "expires_in" => expires_in} = + state, + current_time + ) + when issued_at + expires_in - current_time > 5 do + with {:ok, value} <- set_secret_call(name, value, access_token, state.cred), + true <- is_binary(value) do + {:ok, value, state} + else + _ -> {:error, "Failed to get secret"} + end + end + + defp set_secret(name, value, state, _) do + with {:ok, cred} <- get_service_account_credentials(), + {:ok, %{"access_token" => access_token} = new_state} <- get_access_token(cred), + {:ok, value} <- set_secret_call(name, value, access_token, cred) do + {:ok, value, state |> Map.merge(new_state)} + else + _ -> {:error, "Failed to get secret"} + end + end + defp get_secret_call(name, access_token, cred) do client = http_adpater() @@ -127,6 +174,50 @@ defmodule ExSecrets.Providers.GoogleSecretManager do end end + defp set_secret_call(name, value, access_token, cred) do + client = http_adpater() + + payload = %{ + name: "projects/#{cred["project_id"]}/secrets/#{name}", + replication: %{ + automatic: %{} + } + } + + url = + "https://secretmanager.googleapis.com/v1/projects/#{cred["project_id"]}/secrets?secretId=#{name}" + + with {:ok, %{status_code: status}} when status in [200, 409] <- + client.post(url, Poison.encode!(payload), %{ + "Authorization" => "Bearer #{access_token}", + "content-type" => "application/json" + }), + {:ok, %{status_code: 200}} <- set_secret_version_call(name, value, access_token, cred) do + {:ok, value} + else + _ -> + {:error, "Failed to create secret"} + end + end + + defp set_secret_version_call(name, value, access_token, cred) do + client = http_adpater() + + payload = %{ + payload: %{ + data: Base.encode64(value) + } + } + + url = + "https://secretmanager.googleapis.com/v1/projects/#{cred["project_id"]}/secrets/#{name}:addVersion" + + client.post(url, Poison.encode!(payload), %{ + "Authorization" => "Bearer #{access_token}", + "content-type" => "application/json" + }) + end + defp get_access_token(cred) do client = http_adpater()