Current section
Files
Jump to
Current section
Files
lib/couchdb_auth.ex
defmodule CouchDB do
@moduledoc """
Lightweight CouchDB client builder focused on authentication flows.
The module exposes helpers to build a pre-configured `Tesla.Client`
alongside a thin wrapper around `Tesla.request/2` to keep the original
API surface available.
"""
alias CouchDB.Environment
alias Tesla.Middleware
@type interface :: :base | :backend | :admin
@default_headers [{"user-agent", "couchdb_auth"}]
@default_adapter {Tesla.Adapter.Hackney, []}
@doc """
Build a `Tesla.Client` configured for the requested interface.
The `interface` can be `:base` (default), `:backend`, or `:admin`. Additional
credential overrides may be supplied via `overrides`.
"""
@spec client(interface(), map()) :: Tesla.Client.t()
def client(interface \\ :base, overrides \\ %{}) do
url = interface_url(interface)
overrides = Map.new(overrides)
admin = Environment.server_admin()
proxy_defaults = Environment.proxy_config()
credentials =
%{
username: Map.get(overrides, :username, admin.user),
password: Map.get(overrides, :password, admin.password),
bearer_token: Map.get(overrides, :bearer_token, Environment.bearer_token()),
proxy_user: Map.get(overrides, :proxy_user, proxy_defaults.user),
proxy_roles: Map.get(overrides, :proxy_roles, proxy_defaults.roles),
proxy_secret: Map.get(overrides, :proxy_secret, proxy_defaults.secret),
proxy_token: Map.get(overrides, :proxy_token, proxy_defaults.token),
url: url
}
|> Map.update(:proxy_roles, [], &normalize_roles/1)
|> Map.merge(
Map.drop(overrides, [
:username,
:password,
:bearer_token,
:proxy_user,
:proxy_roles,
:proxy_secret,
:proxy_token
])
)
middleware =
[
{Middleware.BaseUrl, url},
{Middleware.Headers, @default_headers},
{Middleware.JSON, engine: Jason}
] ++ auth_middleware(Environment.auth_type(), credentials, url)
Tesla.client(middleware, @default_adapter)
end
@doc """
Convenience wrapper around `client/2` for admin or cluster endpoints.
"""
@spec admin_client(map()) :: Tesla.Client.t()
def admin_client(overrides \\ %{}) do
client(:admin, overrides)
end
@doc """
Executes an HTTP request using either the provided `%Tesla.Client{}` or a
freshly built one.
"""
@spec request(atom(), String.t(), keyword()) :: {:ok, Tesla.Env.t()} | {:error, any()}
def request(method, url, opts \\ []) do
{client, opts} = pop_client(opts)
request_opts =
opts
|> Keyword.put(:method, method)
|> Keyword.put(:url, url)
Tesla.request(client, request_opts)
end
@doc """
Executes an HTTP request against the admin interface.
"""
@spec admin_request(atom(), String.t(), keyword()) ::
{:ok, Tesla.Env.t()} | {:error, any()}
def admin_request(method, url, opts \\ []) do
opts = Keyword.put(opts, :interface, :admin)
request(method, url, opts)
end
for verb <- ~w(delete get head options patch post put trace)a do
@spec unquote(verb)(String.t(), keyword()) :: {:ok, Tesla.Env.t()} | {:error, any()}
def unquote(verb)(url, opts \\ []) do
request(unquote(verb), url, opts)
end
@spec unquote(:"admin_#{verb}")(String.t(), keyword()) ::
{:ok, Tesla.Env.t()} | {:error, any()}
def unquote(:"admin_#{verb}")(url, opts \\ []) do
admin_request(unquote(verb), url, opts)
end
end
defp pop_client(opts) do
case Keyword.pop(opts, :client) do
{nil, rest_opts} ->
{interface, rest_opts} = Keyword.pop(rest_opts, :interface, :base)
overrides = rest_opts |> Keyword.get(:credentials, %{}) |> Map.new()
rest_opts = Keyword.drop(rest_opts, [:credentials])
{client(interface, overrides), rest_opts}
{client, rest_opts} ->
{client, rest_opts}
end
end
defp interface_url(:backend), do: Environment.backend_url()
defp interface_url(:admin), do: Environment.admin_url()
defp interface_url(_), do: Environment.base_url()
defp auth_middleware(:admin_party, _opts, _url), do: []
defp auth_middleware(:basic, opts, _url) do
[{Tesla.Middleware.CouchDB.BasicAuth, opts}]
end
defp auth_middleware(:cookie, opts, url) do
[{Tesla.Middleware.CouchDB.CookieAuth, Map.put(opts, :url, url)}]
end
defp auth_middleware(:oauth, opts, url) do
credentials =
Environment.oauth_creds()
|> Map.merge(%{url: url})
|> Map.merge(opts)
[{Tesla.Middleware.CouchDB.OAuth, credentials}]
end
defp auth_middleware(:bearer, opts, _url) do
[{Tesla.Middleware.CouchDB.BearerAuth, opts}]
end
defp auth_middleware(:proxy, opts, _url) do
[{Tesla.Middleware.CouchDB.ProxyAuth, opts}]
end
defp auth_middleware(_, _opts, _url), do: []
defp normalize_roles(nil), do: []
defp normalize_roles(roles) when is_list(roles) do
roles
|> Enum.map(&to_string/1)
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
end
defp normalize_roles(roles) when is_binary(roles) do
roles
|> String.split([",", " "], trim: true)
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
end
defp normalize_roles(other), do: normalize_roles([other])
end