lib/permit/resolver.ex

defmodule Permit.Resolver do
  @moduledoc """
  This module is to be considered a private API of the authorization framework.
  It should not be directly used by application code, but rather by wrappers
  providing integration with e.g. Plug or LiveView.
  """
  alias Permit.Types

  @spec authorized_without_preloading?(
          Types.subject(),
          module(),
          Types.resource_module(),
          Types.controller_action(),
          keyword(Types.crud())
        ) :: boolean()
  def authorized_without_preloading?(
        subject,
        authorization_module,
        resource_module,
        live_or_controller_action,
        action_crud_mapping
      )
      when not is_nil(subject) do
    Enum.any?(subject.__struct__.roles(subject), fn role_data ->
      check(
        authorization_module,
        crud_action(live_or_controller_action, action_crud_mapping),
        role_data,
        resource_module,
        subject
      )
    end)
  end

  @spec authorize_with_preloading!(
          Types.subject(),
          module(),
          Types.resource_module(),
          Types.controller_action(),
          keyword(Types.crud()),
          map(),
          function()
        ) :: {:authorized, Ecto.Schema.t()} | :unauthorized
  def authorize_with_preloading!(
        subject,
        authorization_module,
        resource_module,
        live_or_controller_action,
        action_crud_mapping,
        params,
        loader_fn
      )
      when not is_nil(subject) do
    with true <-
           authorized_without_preloading?(
             subject,
             authorization_module,
             resource_module,
             live_or_controller_action,
             action_crud_mapping
           ),
         record when not is_nil(record) <-
           fetch_resource(authorization_module.repo, loader_fn, resource_module, params),
         true <-
           Enum.any?(subject.__struct__.roles(subject), fn role_data ->
             check(
               authorization_module,
               crud_action(live_or_controller_action, action_crud_mapping),
               role_data,
               record,
               subject
             )
           end) do
      {:authorized, record}
    else
      _ -> :unauthorized
    end
  end

  @spec fetch_resource(
          Ecto.Repo.t(),
          function(),
          Types.resource_module(),
          map()
        ) :: struct() | nil
  defp fetch_resource(repo, loader_fn, resource_module, params) do
    id_param_name = "id"
    id_param_value = params[id_param_name]

    loader_fn = loader_fn || default_loader_fn(repo, resource_module, id_param_name)

    case loader_fn.(id_param_value) do
      nil ->
        raise Ecto.NoResultsError, queryable: resource_module

      record ->
        record
    end
  end

  @spec check(
          module(),
          Types.crud(),
          Types.role_record(),
          Types.resource_module() | Types.resource(),
          Types.subject()
        ) :: boolean()
  defp check(authorization_module, crud_action, role_data, resource_or_module, subject)

  defp check(authorization_module, :read, role_data, resource_or_module, subject) do
    authorization_module.can(role_data, subject) |> authorization_module.read?(resource_or_module)
  end

  defp check(authorization_module, :create, role_data, resource_or_module, subject) do
    authorization_module.can(role_data, subject)
    |> authorization_module.create?(resource_or_module)
  end

  defp check(authorization_module, :update, role_data, resource_or_module, subject) do
    authorization_module.can(role_data, subject)
    |> authorization_module.update?(resource_or_module)
  end

  defp check(authorization_module, :delete, role_data, resource_or_module, subject) do
    authorization_module.can(role_data, subject)
    |> authorization_module.delete?(resource_or_module)
  end

  @spec crud_action(atom(), keyword(Types.crud())) :: Types.crud()
  defp crud_action(:index, _opts), do: :read
  defp crud_action(:show, _opts), do: :read
  defp crud_action(:new, _opts), do: :create
  defp crud_action(:create, _opts), do: :create
  defp crud_action(:edit, _opts), do: :update
  defp crud_action(:update, _opts), do: :update
  defp crud_action(:delete, _opts), do: :delete

  defp crud_action(controller_action, action_crud_mapping) do
    action_crud_mapping[controller_action]
  end

  @spec default_loader_fn(Ecto.Repo.t(), Types.resource_module(), Types.id_param_name()) ::
          Types.loader()
  defp default_loader_fn(repo, resource_module, "id") do
    fn id ->
      repo.get(resource_module, id)
    end
  end

  defp default_loader_fn(repo, resource_module, id_param_name) do
    id_param_atom = String.to_existing_atom(id_param_name)

    fn id ->
      repo.get_by(resource_module, [{id_param_atom, id}])
    end
  end
end