lib/auth/graphql.ex

defmodule Rivet.Auth.Graphql do
  @moduledoc """
  Helpers for Graphql resolvers.
  """
  import Rivet.Graphql
  alias Rivet.Auth
  alias Rivet.Ident
  alias Absinthe.Resolution

  ##############################################################################
  @doc """
  Handles extracting the current user from the Absinthe context, and then will
  either call `func` and pass in the current user or return an error if there is
  no user in the context
  """
  def current_user(%Resolution{context: %{user: %{} = u}}),
    do: {:ok, u}

  def current_user(_), do: {:error, :authn}

  def current_user(%Resolution{context: %{user: %{} = user}}, method) do
    if not is_nil(method) do
      graphql_log(method)
    end

    {:ok, user}
  end

  def current_user(_, method), do: graphql_error(method, :authn)

  ########################################################################
  @doc """
  Accept authorization from a user.
  Punt to authz_action(), but log graphql bits.
  """
  def authz_user(
        context,
        assertion \\ %Auth.Assertion{action: :system_admin},
        method \\ nil,
        kwlog \\ []
      )

  def authz_user(info, assertion, method, kwlog) do
    with {:ok, user} <- Rivet.Auth.check_authz(info, assertion) do
      graphql_log(method, kwlog)
      {:ok, user}
    else
      _ ->
        graphql_error(method, :authz)
    end
  end

  ##############################################################################
  @doc """
  check_authz/2 attempts to verify %Auth.Assertion{} against the specified user,
  and if the assertion includes a domain reference, it will attempt to walk
  up the reference tree (if a parent_id exists) and try checking auth again.
  """
  # @spec 
  def check_authz(%{context: %{user: %Ident.User{} = u}}, assertion),
    do: check_authz(u, assertion)

  def check_authz(%Ident.User{} = user, %Auth.Assertion{domain: :global} = assertion) do
    Ident.User.Lib.check_authz(user, assertion)
  end

  def check_authz(%Ident.User{} = user, %Auth.Assertion{domain: domain} = assertion) do
    with {:error, user} <- Ident.User.Lib.check_authz(user, assertion),
         {:ok, %{parent_id: parent_id}} when is_binary(parent_id) <-
           domain.one(id: assertion.ref_id) do
      # walk up the tree, but disable fallback -- if it didn't work before,
      # it won't work again
      check_authz(user, %Auth.Assertion{assertion | fallback: false})
    else
      {:ok, %Ident.User{}} = pass -> pass
      _ -> {:error, user}
    end
  end

  def check_authz(_, _), do: {:error, nil}

  ##############################################################################
  @doc """
  This wraps check_authz with logging and creates normal error output for
  Absinthe to handle.

      with {:ok, authed} <- authz(context, %Rivet.Auth.Assertion{}, "doTheThing") do
        handle success
      end
  """
  @type az_user :: Ident.User.t() | map()
  @type az_assertion :: Auth.Assertion.t()
  @type az_log :: String.t() | nil
  @spec authz(az_user, az_assertion, az_log) ::
          {:ok, Ident.User.t()} | {:ok, map()}

  def authz(meta, assertion, log \\ nil, kwlog \\ [])
  def authz(meta, assertion, nil, _), do: check_authz(meta, assertion)

  def authz(meta, assertion, log, kwlog) do
    with {:ok, user} <- check_authz(meta, assertion) do
      graphql_log(log, kwlog)
      {:ok, user}
    else
      _ ->
        graphql_error(log, :authz)
    end
  end

  @spec action(atom(), keyword()) :: Auth.Assertion.t()
  def action(a, opts \\ []), do: struct(Auth.Assertion, opts ++ [action: a])
end