defmodule ExSecrets.Providers.AzureManagedIdentity do
use ExSecrets.Providers.Base
alias ExSecrets.Utils.Config
@moduledoc """
Azure Key Vault provider provides secrets from an Azure Key Vault through a rest API.
Only the keyvault name is required here once the managed identity has been given access to the keyvault.
```
config :ex_secrets, :providers, %{
azure_managed_identity: %{
key_vault_name: "key-vault-name"
}
```
The provider will handle token renewals and secret fetch.
"""
@headers %{"Content-Type" => "application/x-www-form-urlencoded", "Metadata" => "true"}
@process_name :ex_secrets_azure_managed_identity
def init(_) do
case get_access_token() do
{:ok, data} ->
{:ok, data |> Map.put("issued_at", get_current_epoch())}
_ ->
{:ok, %{}}
end
end
def reset() do
:ok
end
def get(name) do
name = name |> String.split("_") |> Enum.join("-")
with process when not is_nil(process) <-
GenServer.whereis(@process_name) do
GenServer.call(@process_name, {:get, name})
else
nil ->
case get_secret(name, %{}, nil) do
{:ok, value, _} -> value
_ -> nil
end
end
end
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
case get_secret(name, state, get_current_epoch()) do
{:ok, secret, state} -> {:reply, secret, state}
_ -> {:reply, nil, state}
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 token_uri() do
"http://169.254.169.254/metadata/identity/oauth2/token"
|> Kernel.<>("?api-version=2018-02-01&resource=https://vault.azure.net")
end
defp get_secret(
name,
%{"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} <- get_secret_call(name, access_token),
true <- is_binary(value) do
{:ok, value, state}
else
_ -> {:error, "Failed to get secret"}
end
end
defp get_secret(name, state, _) do
with {:ok, %{"access_token" => access_token} = new_state} <- get_access_token(),
{:ok, value} <- get_secret_call(name, access_token) do
{:ok, value, state |> Map.merge(new_state)}
else
_ -> {:error, "Failed to get secret"}
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()
with {:ok, %{body: body, status_code: 200}} <-
name
|> secret_url()
|> client.get(%{"Authorization" => "Bearer #{access_token}"}),
{:ok, %{"value" => value}} <- Poison.decode(body) do
{:ok, value}
else
_ -> {:error, "Failed to get secret"}
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()
with {:ok, %{body: body, status_code: 200}} <-
token_uri()
|> client.get(@headers),
{:ok, data} <- Poison.decode(body) do
{:ok, data |> Map.put("issued_at", get_current_epoch())}
else
_ ->
{:error, "Failed to get access token"}
end
end
defp get_current_epoch() do
System.system_time(:second)
end
defp http_adpater() 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
end