lib/vault/auth/generic.ex

defmodule Vault.Auth.Generic do
  @moduledoc """
  A Generic Auth Adapter. An alternative to writing your own adapter.
  """

  @type request :: %{
          path: String.t(),
          method: :post,
          body: map()
        }

  @type response :: %{
          token: list(String.t()),
          ttl: list(String.t())
        }

  @type params :: %{
          request: request(),
          response: response()
        }

  @behaviour Vault.Auth.Adapter

  @doc """
  Authenticate with a custom auth method. Provide options for the request, and how
  to parse the response.

  ## Examples

  `request` defines parameters for the request to vault
  - `path`- the path for authentication, after "auth" If you want to authenticate against `https://myvault.com/v1/auth/jwt/login`, then the path would be `jwt/login`
  - `method`- one of `:get`, `:post`, `:put`, `:patch`, `:delete`, defaults to `:post`
  - `body`- any params needed to login. Defaults to `%{}`

  `response` defines parameters for parsing the response.
  - `token_path` - a list of properties that describe the JSON path to a token. Defaults to `["auth", "client_token"]`
  - `ttl_path` - a list of properties that describe the JSON path to the ttl, or lease duration. Defaults to ["auth", "lease_duration"]


  The following would provide a minimal adapter for the JWT backend:
  ```
  {:ok, token, ttl} = Vault.Auth.Generic.login(vault, %{
    request: %{
      path: "/jwt/login",
      body: %{role: "my-role", jwt: "my-jwt" },
    }
  })
  ```

  Here's the above example as part of the full Vault client flow. On success,
  it returns an authenticated vault client.
  ```
  vault =
    Vault.new([
      auth: Vault.Auth.Generic,
      http: Vault.HTTP.Tesla,
      engine: Vault.KVV2
    ])

  {:ok, vault} = Vault.auth(vault, %{
    request: %{
      path: "/jwt/login",
      body: %{role: "my-role", jwt: "my-jwt" },
    }
  })
  ```

  Here's a more explicit example, with every option configured.
  ```

  vault =
    Vault.new([
      auth: Vault.Auth.Generic,
      http: Vault.HTTP.Tesla,
      engine: Vault.KVV2
    ])

  {:ok, vault} = Vault.auth(vault, %{
    request:
      path: "/jwt/login",
      method: :post,
      body: %{role: "my-role", jwt: "my-jwt" },
    response: %{
      token: ["auth", "client_token"],
      ttl: ["auth", "lease_duration"]
    }
  })
  ```

  """

  @default_response %{token: ["auth", "client_token"], ttl: ["auth", "lease_duration"]}

  @impl true
  @spec login(Vault.t(), params) :: Vault.Auth.Adapter.response()
  def login(vault, params)

  def login(%Vault{} = vault, %{request: request} = params) do
    request = Map.merge(%{method: :post, body: %{}}, request)

    response = Map.merge(@default_response, Map.get(params, :response, %{}))

    headers = [
      {"Content-Type", "application/json"}
    ]

    url = "auth/#{request.path}"

    with {:ok, http_response} <-
           Vault.HTTP.request(vault, request.method, url, body: request.body, headers: headers) do
      case http_response do
        %{"errors" => []} ->
          {:error, ["Key not found"]}

        %{"errors" => messages} ->
          {:error, messages}

        otherwise ->
          token = get_in(otherwise, response.token)
          ttl = get_in(otherwise, response.ttl)

          if token && ttl do
            {:ok, token, ttl}
          else
            {:error, ["Unexpected response from vault.", otherwise]}
          end
      end
    else
      {:error, reason} ->
        {:error, ["Http adapter error", inspect(reason)]}
    end
  end
end