lib/permit.ex

defmodule Permit do
  @moduledoc """
  Authorization facilities for the application.
  """
  defstruct role: nil, permissions: Permit.Permissions.new(), subject: nil

  alias Permit.Types
  alias Permit.Permissions

  @type t :: %Permit{
          role: Types.role(),
          permissions: Permissions.t(),
          subject: Types.subject() | nil
        }

  defmacro __using__(opts) do
    alias Permit.Types

    permissions_module = Keyword.fetch!(opts, :permissions_module)

    quote do
      @doc """
      Initializes a structure holding permissions for a given user role.

      Returns a Permit struct.
      """

      @spec can(Types.subject_with_role()) :: Permit.t()
      def can(%{role: role} = subject) when is_struct(subject),
        do: can(role, subject)

      @spec can(Types.role_record(), Types.subject() | nil) :: Permit.t()
      def can(role, subject \\ nil)

      def can(role, nil) when is_map(role) do
        unquote(permissions_module).can(role)
      end

      def can(role, subject) when is_map(role) do
        can(role)
        |> Permit.put_subject(subject)
      end

      @spec read?(Permit.t(), Types.resource()) :: boolean()
      def read?(authorization, resource) do
        Permit.verify_record(authorization, resource, :read)
      end

      @spec create?(Permit.t(), Types.resource()) :: boolean()
      def create?(authorization, resource) do
        Permit.verify_record(authorization, resource, :create)
      end

      @spec update?(Permit.t(), Types.resource()) :: boolean()
      def update?(authorization, resource) do
        Permit.verify_record(authorization, resource, :update)
      end

      @spec delete?(Permit.t(), Types.resource()) :: boolean()
      def delete?(authorization, resource) do
        Permit.verify_record(authorization, resource, :delete)
      end

      @spec repo() :: Ecto.Repo.t()
      def repo, do: unquote(opts[:repo])

      @spec accessible_by(Types.subject(), Types.controller_action(), Types.resource()) ::
              {:ok, Ecto.Query.t()} | {:error, term()}
      def accessible_by(current_user, action, resource) do
        unquote(permissions_module)
        |> apply(:can, [current_user])
        |> Map.get(:permissions)
        |> Permissions.construct_query(action, resource)
      end
    end
  end

  @spec put_subject(Permit.t(), Types.role()) :: Permit.t()
  def put_subject(authorization, subject) do
    %Permit{authorization | subject: subject}
  end

  @spec add_permission(Permit.t(), Types.controller_action(), Types.resource_module(), [
          Types.condition()
        ]) ::
          Permit.t()
  def add_permission(authorization, action, resource, conditions) when is_list(conditions) do
    updated_permissions =
      authorization.permissions
      |> Permissions.add(action, resource, conditions)

    %Permit{authorization | permissions: updated_permissions}
  end

  @spec verify_record(Permit.t(), Types.resource(), Types.crud()) :: boolean()
  def verify_record(authorization, record, action) do
    authorization.permissions
    |> Permissions.granted?(action, record, authorization.subject)
  end
end