defmodule Vault.Engine.Generic do
@moduledoc """
A generic Vault.Engine adapter. Most of the vault secret engines don't use a
wildly different API, and can be handled with a single adapter.
## Request Details
By default, `read` runs a GET request, `write` does a POST, `list` does a GET
with an appended `?list=true`, and `delete` runs a DELETE. The options below
should give you additional flexibility.
### Request Options:
- :method - one of :get, :put, :post, :options, :patch, :head
- :full_response - if `true`, returns the full response body on success, rather than just the `data` key. Defaults to `false`,
- :query_params - query params for the request. Defaults to `%{}` (no params)
- :body - body to be sent along with the request. Defaults to `%{}` (no body) on read, or the passed in `value` on write
## Examples
Create a generic vault client:
{:ok, vault } =
Vault.new(
host: System.get_env("VAULT_ADDR"),
auth: Vault.Auth.Token,
engine: Vault.Engine.Generic,
http: Vault.HTTP.Tesla,
) |> Vault.auth(%{token: "token"})
Read/Write from the cubbyhole secret engine.
{:ok, _data} = Vault.write(vault, "cubbyhole/hello", %{"foo" => "bar"})
{:ok, %{"foo" => "bar"}} = Vault.read(vault, "cubbyhole/hello")
Read/Write from the ssh secret engine.
# create a key
{:ok, _} = Vault.write(vault, "ssh/keys/test", %{key: key})
# create a role for that key
{:ok, _} =
Vault.write(vault, "ssh/roles/test", %{
key: "test",
key_type: "dynamic",
default_user: "tester",
admin_user: "admin_tester"
})
# read a role, and return the full response
{:ok, %{ "data" => data } } =
Vault.read(vault, "ssh-client-signer/roles/test", full_response: true)
Options:
- :method - one of :get, :put, :post, :options, :patch, :head
- :full_response - if `true`, returns the full response body on success, rather than just the `data` key. Defaults to `false`,
- :params - query params for the request. Defaults to `%{}` (no params)
- :body - body to be sent along with the request. Defaults to `%{}` (no body) on read, or the passed in `value` on write
"""
@behaviour Vault.Engine.Adapter
@type vault :: Vault.t()
@type path :: String.t()
@type options :: Keyword.t()
@type token :: String.t()
@type value :: map()
@type errors :: list()
@doc """
Gets a value from vault. Defaults to a GET request against the current path.
See `option` details above for full configuration.
"""
@impl true
def read(vault, path, options \\ []) do
options = Keyword.merge([method: :get], options)
request(vault, path, %{}, options)
end
@doc """
Puts a value in vault. Defaults to a POST request against the provided path.
See `options` details above for full configuration.
"""
@impl true
def write(vault, path, value, options \\ []) do
options = Keyword.merge([method: :post], options)
request(vault, path, value, options)
end
@doc """
Lists secrets at a path. Defaults to a GET request against the provided path,
with a query param of ?list=true.
See `options` details above for full configuration.
## Examples
```
{:ok, %{
"keys"=> ["foo", "foo/"]
}
} = Vault.Engine.Generic.list(vault, "path/to/list/", [full_response: true])
```
With the full Response:
```
{:ok, %{
"data" => %{
"keys"=> ["foo", "foo/"]
},
}
} = Vault.Engine.Generic.list(vault, "path/to/list/", [full_response: true])
```
"""
@impl true
def list(vault, path, options \\ []) do
options = Keyword.merge([method: :get, query_params: %{list: true}], options)
request(vault, path, %{}, options)
end
@impl true
def delete(vault, path, options \\ []) do
options = Keyword.merge([method: :delete], options)
request(vault, path, nil, options)
end
defp request(client, path, value, options) do
method = Keyword.get(options, :method, :post)
body = Keyword.get(options, :body, value)
query_params = Keyword.get(options, :query_params, [])
full_response = Keyword.get(options, :full_response, false)
with {:ok, body} <-
Vault.HTTP.request(client, method, path, body: body, query_params: query_params) do
case body do
nil ->
{:ok, %{}}
"" ->
{:ok, %{}}
%{"errors" => []} ->
{:error, ["Key not found"]}
%{"errors" => messages} ->
{:error, messages}
%{} = data when full_response == true ->
{:ok, data}
%{"data" => data} ->
{:ok, data}
otherwise ->
{:error, ["Unknown response from vault", inspect(otherwise)]}
end
else
{:error, reason} ->
{:error, ["Http Adapter error", inspect(reason)]}
end
end
end