lib/macfly.ex

defmodule Macfly do
  @moduledoc """
  Macfly is a library for managing Fly macaroons.
  """
  alias Macfly.Macaroon
  alias Macfly.Options
  alias Macfly.Caveat
  alias Macfly.Discharge
  alias Macfly.Caveat.ThirdParty
  alias Macfly.Nonce

  @doc """
  Decode a macaroon header into a list of Macaroon structs.
  """
  @spec decode(String.t(), Options.t()) :: {:ok, list(Macaroon.t())} | {:error, any()}
  def decode(header, o \\ %Options{})
  def decode("FlyV1 " <> toks, %Options{} = o), do: decode(toks, o)
  def decode("Bearer " <> toks, %Options{} = o), do: decode(toks, o)

  def decode(<<tokens::binary>>, %Options{} = o) do
    tokens
    |> String.split(",")
    |> decode_tokens(o)
  end

  defp decode_tokens([token | rest], %Options{} = o) do
    with {:ok, macaroon} <- Macaroon.decode(token, o),
         {:ok, rest} <- decode_tokens(rest, o) do
      {:ok, [macaroon | rest]}
    else
      error -> error
    end
  end

  defp decode_tokens([], %Options{}), do: {:ok, []}

  @doc """
  Encode a list of Macaroon structs into a macaroon header.
  """
  @spec encode(list(Macaroon.t())) :: String.t()
  def encode(macaroons) do
    macaroons
    |> Enum.map(&Macaroon.encode/1)
    |> Enum.join(",")
    |> then(&("FlyV1 " <> &1))
  end

  @doc """
  Attenuate the permission tokens within the list of macaroons.
  """
  @spec attenuate(list(Macaroon.t()), list(Caveat), Options.t()) :: list(Macaroon.t())
  def attenuate(macaroons, caveats, options \\ %Options{})
  def attenuate([], _, _), do: []

  def attenuate(
        [%Macaroon{location: location} = m | rest],
        caveats,
        %Options{
          location: target_location
        } = o
      )
      when location == target_location do
    [Macaroon.attenuate(m, caveats) | attenuate(rest, caveats, o)]
  end

  def attenuate([m | rest], caveats, %Options{} = o) do
    [m | attenuate(rest, caveats, o)]
  end

  @doc """
  Get list of Discharge structs from list of macaroons for use in discharging
  third party caveats.
  """
  @spec discharges(list(Macaroon.t()), Options.t()) :: list(Discharge.t())
  def discharges(macaroons, %Options{} = o \\ %Options{}) do
    for %ThirdParty{location: l, ticket: t} <- undischarged_tp_caveats(macaroons, o) do
      Discharge.new(location: l, ticket: t)
    end
  end

  @spec undischarged_tp_caveats(list(Macaroon.t()), Options.t()) :: list({binary(), String.t()})
  defp undischarged_tp_caveats(macaroons, %Options{location: location}) do
    # Create mapping from tp ticket to either the tp caveat that it came from or
    # the discharge macaroon that discharges it. At the end, the tickets mapping
    # to a tp caveat are necessarily undischarged.
    for m <- macaroons, reduce: %{} do
      acc ->
        case m do
          %Macaroon{location: ^location} ->
            for %ThirdParty{ticket: ticket} = c <- m.caveats, reduce: acc do
              acc -> Map.put_new(acc, ticket, c)
            end

          %Macaroon{nonce: %Nonce{kid: ticket}} ->
            Map.put(acc, ticket, m)
        end
    end
    |> then(&for {_, %ThirdParty{} = tp} <- &1, do: tp)
  end

  @spec default_options() :: Options.t()
  def default_options(), do: %Options{}
end