From c16f23fd4ae95bd48c15962d2b2e29cfeb116e07 Mon Sep 17 00:00:00 2001 From: Zemuldo Date: Sat, 28 Oct 2023 20:07:59 +0300 Subject: [PATCH] adds set secret azure kv and managed identity --- lib/ex_secrets.ex | 34 ++++------ lib/providers/azure_key_vault.ex | 81 +++++++++++++++++++++--- lib/providers/azure_managed_identity.ex | 82 ++++++++++++++++++++++--- 3 files changed, 162 insertions(+), 35 deletions(-) diff --git a/lib/ex_secrets.ex b/lib/ex_secrets.ex index 1ba2b4a..2f73d1b 100644 --- a/lib/ex_secrets.ex +++ b/lib/ex_secrets.ex @@ -106,16 +106,7 @@ defmodule ExSecrets do true -> :ok end - with true <- is_atom(provider), - value when not is_nil(value) <- Cache.get(key) do - value - else - nil -> - Cache.pass_by( - key, - SecretFetchLimiter.allow(key, ExSecrets, :get_using_provider, [key, provider]) - ) - end + get(key, provider: provider) end @doc """ @@ -123,18 +114,19 @@ defmodule ExSecrets do """ @deprecated "This function is deprecated. Use get/2 instead." def get(key, provider, default) do - with true <- is_atom(provider), - value when not is_nil(value) <- Cache.get(key) do - value + case get(key, provider: provider) do + nil -> default + value -> value + end + end + + def set(provider, key, value) do + with provider when is_atom(provider) <- Resolver.call(provider) do + Kernel.apply(provider, :set, [key, value]) + Cache.save(key, value) else - nil -> - case Cache.pass_by( - key, - SecretFetchLimiter.allow(key, ExSecrets, :get_using_provider, [key, provider]) - ) do - nil -> default - value -> value - end + {:error, message} -> {:error, message} + _ -> {:error, :provider_not_found} end end diff --git a/lib/providers/azure_key_vault.ex b/lib/providers/azure_key_vault.ex index 18adce2..ed95a0c 100644 --- a/lib/providers/azure_key_vault.ex +++ b/lib/providers/azure_key_vault.ex @@ -113,8 +113,22 @@ defmodule ExSecrets.Providers.AzureKeyVault 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, _} -> + value + + _ -> + {:error, "Could not set secret, check credentials and permissions"} + end + end end def handle_call({:get, name}, _from, state) do @@ -124,6 +138,13 @@ defmodule ExSecrets.Providers.AzureKeyVault 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, secret, state} + _ -> {:reply, nil, state} + end + end + defp token_uri(tenant_id) do "https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/token" end @@ -148,6 +169,32 @@ defmodule ExSecrets.Providers.AzureKeyVault do get_access_token(), {:ok, value} <- get_secret_call(name, access_token) do {:ok, value, state |> Map.merge(new_state) |> Map.put("issued_at", get_current_epoch())} + else + err -> err + 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), + true <- is_binary(value) do + {:ok, value, state} + else + err -> err + end + end + + defp set_secret(name, value, state, _) do + with {:ok, %{"access_token" => access_token} = new_state} <- + get_access_token(), + {:ok, value} <- set_secret_call(name, value, access_token) do + {:ok, value, state |> Map.merge(new_state) |> Map.put("issued_at", get_current_epoch())} else _ -> {:error, "Failed to get secret"} end @@ -155,13 +202,11 @@ defmodule ExSecrets.Providers.AzureKeyVault do defp get_secret_call(name, access_token) do client = http_adpater() - key_vault_name = Config.provider_config_value(:azure_key_vault, :key_vault_name) with {:ok, %{body: body, status_code: 200}} <- - client.get( - "https://#{key_vault_name}.vault.azure.net/secrets/#{name}?api-version=2016-10-01", - %{"Authorization" => "Bearer #{access_token}"} - ), + name + |> secret_url() + |> client.get(%{"Authorization" => "Bearer #{access_token}"}), {:ok, %{"value" => value}} <- Poison.decode(body) do {:ok, value} else @@ -169,6 +214,23 @@ defmodule ExSecrets.Providers.AzureKeyVault do end end + defp set_secret_call(name, value, access_token) do + client = http_adpater() + + with {:ok, %{body: body, status_code: 200}} <- + name + |> secret_url() + |> client.put(Poison.encode!(%{value: value}), %{ + "Authorization" => "Bearer #{access_token}", + "content-type" => "application/json" + }), + {:ok, %{"value" => value}} <- Poison.decode(body) do + {:ok, value} + else + err -> err + end + end + defp get_access_token() do client = http_adpater() client_secret = Config.provider_config_value(:azure_key_vault, :client_secret) @@ -280,6 +342,11 @@ defmodule ExSecrets.Providers.AzureKeyVault do Application.get_env(:ex_secrets, :http_adapter, HTTPoison) end + defp secret_url(name) do + key_vault_name = Config.provider_config_value(:azure_key_vault, :key_vault_name) + "https://#{key_vault_name}.vault.azure.net/secrets/#{name}?api-version=2016-10-01" + end + def process_name() do @process_name end diff --git a/lib/providers/azure_managed_identity.ex b/lib/providers/azure_managed_identity.ex index 5884e3e..fc72920 100644 --- a/lib/providers/azure_managed_identity.ex +++ b/lib/providers/azure_managed_identity.ex @@ -50,8 +50,22 @@ defmodule ExSecrets.Providers.AzureManagedIdentity 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, _} -> + value + + _ -> + {:error, "Could not set secret, check credentials and permissions"} + end + end end def handle_call({:get, name}, _from, state) do @@ -61,6 +75,13 @@ defmodule ExSecrets.Providers.AzureManagedIdentity 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, secret, state} + _ -> {:reply, nil, state} + end + end + defp token_uri() do "http://169.254.169.254/metadata/identity/oauth2/token" |> Kernel.<>("?api-version=2018-02-01&resource=https://vault.azure.net") @@ -90,15 +111,40 @@ defmodule ExSecrets.Providers.AzureManagedIdentity do end end + def 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), + true <- is_binary(value) do + {:ok, value, state} + else + err -> + err + end + end + + def set_secret(name, value, state, _) do + with {:ok, %{"access_token" => access_token} = new_state} <- + get_access_token(), + {:ok, value} <- set_secret_call(name, value, access_token) do + {:ok, value, state |> Map.merge(new_state) |> Map.put("issued_at", get_current_epoch())} + else + _ -> {:error, "Failed to get secret"} + end + end + defp get_secret_call(name, access_token) do client = http_adpater() - key_vault_name = Config.provider_config_value(:azure_managed_identity, :key_vault_name) with {:ok, %{body: body, status_code: 200}} <- - client.get( - "https://#{key_vault_name}.vault.azure.net/secrets/#{name}?api-version=2016-10-01", - %{"Authorization" => "Bearer #{access_token}"} - ), + name + |> secret_url() + |> client.get(%{"Authorization" => "Bearer #{access_token}"}), {:ok, %{"value" => value}} <- Poison.decode(body) do {:ok, value} else @@ -106,6 +152,23 @@ defmodule ExSecrets.Providers.AzureManagedIdentity do end end + defp set_secret_call(name, value, access_token) do + client = http_adpater() + + with {:ok, %{body: body, status_code: 200}} <- + name + |> secret_url() + |> client.put(Poison.encode!(%{value: value}), %{ + "Authorization" => "Bearer #{access_token}", + "content-type" => "application/json" + }), + {:ok, %{"value" => value}} <- Poison.decode(body) do + {:ok, value} + else + err -> err + end + end + defp get_access_token() do client = http_adpater() @@ -128,6 +191,11 @@ defmodule ExSecrets.Providers.AzureManagedIdentity do Application.get_env(:ex_secrets, :http_adapter, HTTPoison) end + defp secret_url(name) do + key_vault_name = Config.provider_config_value(:azure_managed_identity, :key_vault_name) + "https://#{key_vault_name}.vault.azure.net/secrets/#{name}?api-version=2016-10-01" + end + def process_name() do @process_name end