Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#45 #46 Update elixir ver to 14, use mix format, fix ssl issue #44

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 2.0.0

* fixed wrong header sent with AWS IAM scenarios
* updated to Elixir 1.14 standards and formatting, per PR from lauragrechenko
* considering this a "breaking change" only because of the updated Elixir version
requirements
* update all dependecies to latest versions as of 2023-08-23
* removed unused dependencies from mix.lock
* put notice in README about this fork

## 1.0.1
* Mark eliver as dev dependency

Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ A very simple elixir client that authenticates, reads, writes, and deletes
secrets from HashiCorp's Vault. As listed on [Vault
Libraries](https://www.vaultproject.io/api/libraries.html#elixir).

This is **fork** from the original vaultex library at
[findmypast/vaultex](https://github.com/findmypast/vaultex), which appears to be abandoned
by the author at this time.

## Installation

The package can be installed as:
Expand All @@ -15,7 +19,7 @@ The package can be installed as:

```elixir
def deps do
[{:vaultex, "~> 0.8"}]
[{:vaultex, "~> 2.0"}]
end
```
2) Ensure `vaultex` is started before your application:
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.1
2.0.0
7 changes: 3 additions & 4 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
import Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
Expand All @@ -18,8 +18,7 @@ use Mix.Config
#
# Or configure a 3rd-party app:
#
# config :logger, level: :info
#
# config :logger, :console, level: :info

# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
Expand All @@ -31,4 +30,4 @@ use Mix.Config

config :vaultex, httpoison: HTTPoison

import_config "#{Mix.env}.exs"
import_config "#{Mix.env()}.exs"
2 changes: 1 addition & 1 deletion config/dev.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use Mix.Config
import Config

config :vaultex, httpoison: HTTPoison
config :vaultex, app_id: "foo"
Expand Down
2 changes: 1 addition & 1 deletion config/prod.exs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
use Mix.Config
import Config

config :vaultex, httpoison: HTTPoison
2 changes: 1 addition & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use Mix.Config
import Config

config :vaultex, httpoison: Vaultex.Test.TestDoubles.MockHTTPoison
config :vaultex, app_id: "foo"
Expand Down
189 changes: 141 additions & 48 deletions lib/test_doubles/mock_httpoison.ex
Original file line number Diff line number Diff line change
@@ -1,82 +1,175 @@
defmodule Vaultex.Test.TestDoubles.MockHTTPoison do

def request(:post, url, params, _, _) do
status_code = status_code(url, params)
vault_response(params, status_code, url)
end

def request(:get, url, _params, [{"X-Vault-Token", token}, {"Content-Type", "application/json"}], _opts) do
def request(
:get,
url,
_params,
[{"X-Vault-Token", token}, {"Content-Type", "application/json"}],
_opts
) do
vault_response(token, "whatever", url)
end

def request(:get, url, _params, _, _) do
cond do
url |> String.contains?("secret/foo") -> {:ok, %{status_code: status_code(url, url),
headers: [{"Location", redirect_url(url)}],
body: Poison.encode!(%{"data" => %{"value" => "bar"}},[])}}
url |> String.contains?("secret/dynamic/foo") -> {:ok, %{status_code: status_code(url, url),
headers: [{"Location", redirect_url(url)}],
body: Poison.encode!(%{"lease_id" => "secret/dynamic/foo/b4z",
"lease_duration" => 60,
"renewable" => true,
"data" => %{"value" => "bar"}},[])}}
url |> String.contains?("secret/empty_warning") -> {:ok, %{status_code: status_code(url, url),
headers: [{"Location", redirect_url(url)}],
body: Poison.encode!(%{"data" => %{"value" => "empty_warning"}, "warnings" => nil},[])}}
url |> String.contains?("secret/empty_error") -> {:ok, %{status_code: status_code(url, url),
headers: [{"Location", redirect_url(url)}],
body: Poison.encode!(%{"data" => %{"value" => "empty_error"}, "errors" => nil},[])}}
url |> String.contains?("secret/coffee") -> {:ok, %{status_code: 404, body: Poison.encode!(%{warnings: ["bad path"]})}}
url |> String.contains?("secret/baz") -> {:ok, %{status_code: "whatever", body: Poison.encode!(%{"errors" => []},[])}}
url |> String.contains?("secret/baz") -> {:ok, %{status_code: "whatever", body: Poison.encode!(%{"errors" => []},[])}}
url |> String.contains?("secret/faz") -> {:ok, %{status_code: "whatever", body: Poison.encode!(%{"errors" => ["Not Authenticated"]},[])}}
url |> String.contains?("secret/boom") -> {:error, %HTTPoison.Error{id: nil, reason: :econnrefused}}
:else -> {:ok, %{} }
url |> String.contains?("secret/foo") ->
{:ok,
%{
status_code: status_code(url, url),
headers: [{"Location", redirect_url(url)}],
body: Poison.encode!(%{"data" => %{"value" => "bar"}}, [])
}}

url |> String.contains?("secret/dynamic/foo") ->
{:ok,
%{
status_code: status_code(url, url),
headers: [{"Location", redirect_url(url)}],
body:
Poison.encode!(
%{
"lease_id" => "secret/dynamic/foo/b4z",
"lease_duration" => 60,
"renewable" => true,
"data" => %{"value" => "bar"}
},
[]
)
}}

url |> String.contains?("secret/empty_warning") ->
{:ok,
%{
status_code: status_code(url, url),
headers: [{"Location", redirect_url(url)}],
body: Poison.encode!(%{"data" => %{"value" => "empty_warning"}, "warnings" => nil}, [])
}}

url |> String.contains?("secret/empty_error") ->
{:ok,
%{
status_code: status_code(url, url),
headers: [{"Location", redirect_url(url)}],
body: Poison.encode!(%{"data" => %{"value" => "empty_error"}, "errors" => nil}, [])
}}

url |> String.contains?("secret/coffee") ->
{:ok, %{status_code: 404, body: Poison.encode!(%{warnings: ["bad path"]})}}

url |> String.contains?("secret/baz") ->
{:ok, %{status_code: "whatever", body: Poison.encode!(%{"errors" => []}, [])}}

url |> String.contains?("secret/baz") ->
{:ok, %{status_code: "whatever", body: Poison.encode!(%{"errors" => []}, [])}}

url |> String.contains?("secret/faz") ->
{:ok,
%{
status_code: "whatever",
body: Poison.encode!(%{"errors" => ["Not Authenticated"]}, [])
}}

url |> String.contains?("secret/boom") ->
{:error, %HTTPoison.Error{id: nil, reason: :econnrefused}}

:else ->
{:ok, %{}}
end
end

def request(:put, url, _params, _, _) do
cond do
String.ends_with?(url, "secret/foo/withresponse") -> {:ok, %{status_code: 200, body: Poison.encode!(%{"data" => %{"value" => "bar"}},[])}}
String.contains?(url, "sys/leases/renew") -> {:ok, %{status_code: 200, body: Poison.encode!(%{"lease_id" => "secret/dynamic/foo/b4z",
"lease_duration" => 160,
"renewable" => true},[])}}
String.ends_with?(url, "secret/foo") -> {:ok, %{status_code: 204, body: ""}}
String.ends_with?(url, "secret/foo/redirects") -> {:ok, %{status_code: 307, body: "", headers: [{"Location", "secret/foo"}]}}
:else -> raise "Unmatched url #{url}"
String.ends_with?(url, "secret/foo/withresponse") ->
{:ok, %{status_code: 200, body: Poison.encode!(%{"data" => %{"value" => "bar"}}, [])}}

String.contains?(url, "sys/leases/renew") ->
{:ok,
%{
status_code: 200,
body:
Poison.encode!(
%{
"lease_id" => "secret/dynamic/foo/b4z",
"lease_duration" => 160,
"renewable" => true
},
[]
)
}}

String.ends_with?(url, "secret/foo") ->
{:ok, %{status_code: 204, body: ""}}

String.ends_with?(url, "secret/foo/redirects") ->
{:ok, %{status_code: 307, body: "", headers: [{"Location", "secret/foo"}]}}

:else ->
raise "Unmatched url #{url}"
end
end

def request(:delete, url, _params, [{"X-Vault-Token", token}, {"Content-Type", "application/json"}], _opts) do
def request(
:delete,
url,
_params,
[{"X-Vault-Token", token}, {"Content-Type", "application/json"}],
_opts
) do
vault_response(token, "whatever", url)
end

def request(:delete, url, _params, _, _) do
cond do
url |> String.contains?("secret/foo") -> {:ok, %{status_code: 204,
headers: [{"Location", redirect_url(url)}],
body: Poison.encode!(%{},[])}}
url |> String.contains?("secret/baz") -> {:ok, %{status_code: 204, body: Poison.encode!(%{"errors" => []},[])}}
url |> String.contains?("secret/faz") -> {:ok, %{status_code: 204, body: Poison.encode!(%{"errors" => ["Not Authenticated"]},[])}}
url |> String.contains?("secret/boom") -> {:error, %HTTPoison.Error{id: nil, reason: :econnrefused}}
:else -> {:ok, %{} }
url |> String.contains?("secret/foo") ->
{:ok,
%{
status_code: 204,
headers: [{"Location", redirect_url(url)}],
body: Poison.encode!(%{}, [])
}}

url |> String.contains?("secret/baz") ->
{:ok, %{status_code: 204, body: Poison.encode!(%{"errors" => []}, [])}}

url |> String.contains?("secret/faz") ->
{:ok, %{status_code: 204, body: Poison.encode!(%{"errors" => ["Not Authenticated"]}, [])}}

url |> String.contains?("secret/boom") ->
{:error, %HTTPoison.Error{id: nil, reason: :econnrefused}}

:else ->
{:ok, %{}}
end
end

defp vault_response(key, status_code, url) do
cond do
key |> String.contains?("good") -> {:ok, %{status_code: status_code,
headers: [{"Location", redirect_url(url)}],
body: Poison.encode!(%{"auth" => %{"client_token" => "mytoken"}}, [])}}
key |> String.contains?("boom") -> {:error, %HTTPoison.Error{id: nil, reason: :econnrefused}}
key |> String.contains?("ssl") -> {:error, %HTTPoison.Error{id: nil, reason: {:tls_alert, 'unknown ca'}}}
:else -> {:ok, %{body: Poison.encode!(%{errors: ["Not Authenticated"] }, [])}}
key |> String.contains?("good") ->
{:ok,
%{
status_code: status_code,
headers: [{"Location", redirect_url(url)}],
body: Poison.encode!(%{"auth" => %{"client_token" => "mytoken"}}, [])
}}

key |> String.contains?("boom") ->
{:error, %HTTPoison.Error{id: nil, reason: :econnrefused}}

key |> String.contains?("ssl") ->
{:error, %HTTPoison.Error{id: nil, reason: {:tls_alert, ~c"unknown ca"}}}

:else ->
{:ok, %{body: Poison.encode!(%{errors: ["Not Authenticated"]}, [])}}
end
end

defp status_code(url, stringified_params) do
if String.contains? stringified_params, "redirect" do
if redirected_url? url do
if String.contains?(stringified_params, "redirect") do
if redirected_url?(url) do
200
else
307
Expand All @@ -90,7 +183,7 @@ defmodule Vaultex.Test.TestDoubles.MockHTTPoison do
"#{url}/redirected"
end

defp redirected_url?(url)do
String.ends_with? url, "redirected"
defp redirected_url?(url) do
String.ends_with?(url, "redirected")
end
end
2 changes: 1 addition & 1 deletion lib/vaultex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule Vaultex do

children = [
# Define workers and child supervisors to be supervised
worker(Vaultex.Client, []),
Vaultex.Client
]

# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
Expand Down
19 changes: 14 additions & 5 deletions lib/vaultex/auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,30 @@ defmodule Vaultex.Auth do

# auth method with usernames are expected to call `POST auth/:method/login/:username`
def handle(method, %{username: username} = credentials, state) do
request(:post, "#{state.url}auth/#{method}/login/#{username}", credentials, [{"Content-Type", "application/json"}])
request(:post, "#{state.url}auth/#{method}/login/#{username}", credentials, [
{"Content-Type", "application/json"}
])
|> handle_response(state)
end

# Generic login behavior for most methods
def handle(method, credentials, state) when is_map(credentials) do
request(:post, "#{state.url}auth/#{method}/login", credentials, [{"Content-Type", "application/json"}])
request(:post, "#{state.url}auth/#{method}/login", credentials, [
{"Content-Type", "application/json"}
])
|> handle_response(state)
end

defp handle_response({:ok, response}, state) do
case response.body |> Poison.decode!() do
%{"errors" => messages} -> {:reply, {:error, messages}, state}
%{"auth" => nil, "data" => data} -> {:reply, {:ok, :authenticated}, Map.merge(state, %{token: data["id"]})}
%{"auth" => properties} -> {:reply, {:ok, :authenticated}, Map.merge(state, %{token: properties["client_token"]})}
%{"errors" => messages} ->
{:reply, {:error, messages}, state}

%{"auth" => nil, "data" => data} ->
{:reply, {:ok, :authenticated}, Map.merge(state, %{token: data["id"]})}

%{"auth" => properties} ->
{:reply, {:ok, :authenticated}, Map.merge(state, %{token: properties["client_token"]})}
end
end

Expand Down
15 changes: 9 additions & 6 deletions lib/vaultex/aws_iam.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ defmodule Vaultex.Auth.AWSIAM do
end

defp request_headers(server) do
base_headers = [
{"User-Agent", "ExAws"},
{"Content-Type", "application/x-www-form-urlencoded"}
]
|> maybe_add_server(server)
base_headers =
[
{"User-Agent", "ExAws"},
{"Content-Type", "application/x-www-form-urlencoded"}
]
|> maybe_add_server(server)

config = ExAws.Config.new(:sts)

Expand All @@ -33,10 +34,12 @@ defmodule Vaultex.Auth.AWSIAM do
end

defp maybe_add_role(credentials, nil), do: credentials

defp maybe_add_role(credentials, role),
do: Map.put(credentials, :role, role)

defp maybe_add_server(headers, nil), do: headers

defp maybe_add_server(headers, server),
do: [{"X-Vault-awsiam-Server-Id", server} | headers]
do: [{"X-Vault-AWS-IAM-Server-Id", server} | headers]
end
Loading