lib/ash/policy/policy.ex

defmodule Ash.Policy.Policy do
  @moduledoc "The data structure for a policy, and functions for working with them."
  # For now we just write to `checks` and move them to `policies`
  # on build, when we support nested policies we can change that.
  defstruct [
    :condition,
    :policies,
    :bypass?,
    :checks,
    :description,
    :access_type
  ]

  @type t :: %__MODULE__{}

  def solve(authorizer) do
    authorizer.policies
    |> build_requirements_expression(authorizer.facts)
    |> Ash.Policy.SatSolver.solve()
  end

  defp build_requirements_expression(policies, facts) do
    at_least_one_policy_expression = at_least_one_policy_expression(policies, facts)

    policy_expression =
      {:and, at_least_one_policy_expression, compile_policy_expression(policies, facts)}

    facts_expression = Ash.Policy.SatSolver.facts_to_statement(Map.drop(facts, [true, false]))

    if facts_expression do
      {:and, facts_expression, policy_expression}
    else
      policy_expression
    end
  end

  def at_least_one_policy_expression(policies, facts) do
    policies
    |> Enum.map(&condition_expression(&1.condition, facts))
    |> Enum.filter(& &1)
    |> Enum.reduce(false, fn condition, acc ->
      {:or, condition, acc}
    end)
  end

  def fetch_fact(facts, %{check_module: mod, check_opts: opts}) do
    fetch_fact(facts, {mod, opts})
  end

  def fetch_fact(facts, {mod, opts}) do
    # TODO: this is slow, and we should figure out a better way to access facts indiscriminate of access type,
    # which my necessity must be stored with the fact (as facts create scenarios)
    # Eventually we may just want to track two separate maps of facts, one with access type and one without
    Enum.find_value(facts, fn
      {{fact_mod, fact_opts}, result} ->
        if mod == fact_mod &&
             Keyword.delete(fact_opts, :access_type) ==
               Keyword.delete(opts, :access_type) do
          {:ok, result}
        end

      _ ->
        nil
    end)
    |> case do
      nil ->
        :error

      value ->
        value
    end
  end

  defp condition_expression(condition, facts) do
    condition
    |> List.wrap()
    |> Enum.reduce(nil, fn
      condition, nil ->
        case fetch_fact(facts, condition) do
          {:ok, true} ->
            true

          {:ok, false} ->
            false

          _ ->
            condition
        end

      _condition, false ->
        false

      condition, expression ->
        case fetch_fact(facts, condition) do
          {:ok, true} ->
            expression

          {:ok, false} ->
            false

          _ ->
            {:and, condition, expression}
        end
    end)
  end

  defp compile_policy_expression(policies, facts)

  defp compile_policy_expression([], _facts) do
    false
  end

  defp compile_policy_expression(
         [%__MODULE__{condition: condition, policies: policies}],
         facts
       ) do
    compiled_policies = compile_policy_expression(policies, facts)
    condition_expression = condition_expression(condition, facts)

    case condition_expression do
      true ->
        compiled_policies

      false ->
        true

      nil ->
        compiled_policies

      condition_expression ->
        {:and, condition_expression, compiled_policies}
    end
  end

  defp compile_policy_expression(
         [
           %__MODULE__{condition: condition, policies: policies, bypass?: bypass?} | rest
         ],
         facts
       ) do
    condition_expression = condition_expression(condition, facts)

    case condition_expression do
      true ->
        if bypass? do
          {:or, compile_policy_expression(policies, facts),
           compile_policy_expression(rest, facts)}
        else
          {:and, compile_policy_expression(policies, facts),
           compile_policy_expression(rest, facts)}
        end

      false ->
        compile_policy_expression(rest, facts)

      nil ->
        if bypass? do
          {:or, compile_policy_expression(policies, facts),
           compile_policy_expression(rest, facts)}
        else
          {:and, compile_policy_expression(policies, facts),
           compile_policy_expression(rest, facts)}
        end

      condition_expression ->
        if bypass? do
          {:or, {:and, condition_expression, compile_policy_expression(policies, facts)},
           compile_policy_expression(rest, facts)}
        else
          {:or, {:and, condition_expression, compile_policy_expression(policies, facts)},
           {:and, {:not, condition_expression}, compile_policy_expression(rest, facts)}}
        end
    end
  end

  defp compile_policy_expression(
         [%{type: :authorize_if} = clause],
         facts
       ) do
    case fetch_fact(facts, clause) do
      {:ok, true} ->
        true

      {:ok, false} ->
        false

      :error ->
        {clause.check_module, clause.check_opts}
    end
  end

  defp compile_policy_expression(
         [%{type: :authorize_if} = clause | rest],
         facts
       ) do
    case fetch_fact(facts, clause) do
      {:ok, true} ->
        true

      {:ok, false} ->
        compile_policy_expression(rest, facts)

      :error ->
        {:or, {clause.check_module, clause.check_opts}, compile_policy_expression(rest, facts)}
    end
  end

  defp compile_policy_expression(
         [%{type: :authorize_unless} = clause],
         facts
       ) do
    case fetch_fact(facts, clause) do
      {:ok, true} ->
        false

      {:ok, false} ->
        true

      :error ->
        {clause.check_module, clause.check_opts}
    end
  end

  defp compile_policy_expression(
         [%{type: :authorize_unless} = clause | rest],
         facts
       ) do
    case fetch_fact(facts, clause) do
      {:ok, true} ->
        compile_policy_expression(rest, facts)

      {:ok, false} ->
        true

      :error ->
        {:or, {clause.check_module, clause.check_opts}, compile_policy_expression(rest, facts)}
    end
  end

  defp compile_policy_expression([%{type: :forbid_if}], _facts) do
    false
  end

  defp compile_policy_expression(
         [%{type: :forbid_if} = clause | rest],
         facts
       ) do
    case fetch_fact(facts, clause) do
      {:ok, true} ->
        false

      {:ok, false} ->
        compile_policy_expression(rest, facts)

      :error ->
        {:and, {:not, {clause.check_module, clause.check_opts}},
         compile_policy_expression(rest, facts)}
    end
  end

  defp compile_policy_expression([%{type: :forbid_unless}], _facts) do
    false
  end

  defp compile_policy_expression(
         [%{type: :forbid_unless} = clause | rest],
         facts
       ) do
    case fetch_fact(facts, clause) do
      {:ok, true} ->
        compile_policy_expression(rest, facts)

      {:ok, false} ->
        false

      :error ->
        {:and, {clause.check_module, clause.check_opts}, compile_policy_expression(rest, facts)}
    end
  end
end