lib/ash/policy/policy.ex

defmodule Ash.Policy.Policy do
  @moduledoc "Represents a policy on an Ash.Resource"

  # 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__{}

  @static_checks [
    {Ash.Policy.Check.Static, [result: true]},
    {Ash.Policy.Check.Static, [result: false]},
    true,
    false
  ]

  def solve(authorizer) do
    authorizer.policies
    |> build_requirements_expression(authorizer)
    |> case do
      {true, authorizer} ->
        {:ok, true, authorizer}

      {false, authorizer} ->
        {:ok, false, authorizer}

      {expression, authorizer} ->
        Ash.Policy.SatSolver.solve(expression, fn scenario, bindings ->
          scenario
          |> Ash.SatSolver.solutions_to_predicate_values(bindings)
          |> Map.drop(@static_checks)
        end)
        |> case do
          {:ok, scenarios} ->
            {:ok, scenarios, authorizer}

          {:error, error} ->
            {:error, authorizer, error}
        end
    end
  catch
    {:error, authorizer, error} ->
      {:error, authorizer, error}
  end

  @doc false
  def transform(policy) do
    if Enum.empty?(policy.policies) do
      {:error, "Policies must have at least one check."}
    else
      {:ok, policy}
    end
  end

  defp build_requirements_expression(policies, authorizer) do
    {at_least_one_policy_expression, authorizer} =
      at_least_one_policy_expression(policies, authorizer)

    if at_least_one_policy_expression == false do
      {false, authorizer}
    else
      {policy_expression, authorizer} =
        if at_least_one_policy_expression == true do
          compile_policy_expression(policies, authorizer)
        else
          {policy_expression, authorizer} = compile_policy_expression(policies, authorizer)

          case {:and, at_least_one_policy_expression, policy_expression} do
            {:and, false, _} ->
              {false, authorizer}

            {:and, _, false} ->
              {false, authorizer}

            {:and, true, true} ->
              {true, authorizer}

            {:and, left, true} ->
              {left, authorizer}

            {:and, true, right} ->
              {right, authorizer}

            other ->
              {other, authorizer}
          end
        end

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

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

  def at_least_one_policy_expression(policies, authorizer) do
    policies
    |> List.wrap()
    |> Enum.reduce({[], authorizer}, fn
      policy, {condition_exprs, authorizer} when is_list(condition_exprs) ->
        case condition_expression(policy.condition, authorizer) do
          {nil, authorizer} ->
            {condition_exprs, authorizer}

          {true, authorizer} ->
            {true, authorizer}

          {condition_expr, authorizer} ->
            {[condition_expr | condition_exprs], authorizer}
        end

      _, {true, authorizer} ->
        {true, authorizer}
    end)
    |> then(fn
      {true, authorizer} ->
        {true, authorizer}

      {condition_exprs, authorizer} ->
        {condition_exprs
         |> Enum.reduce(false, fn
           _, true ->
             true

           true, _ ->
             true

           false, acc ->
             acc

           condition, acc ->
             {:or, condition, acc}
         end), authorizer}
    end)
  end

  def fetch_or_strict_check_fact(authorizer, %{check_module: mod, check_opts: opts}) do
    fetch_or_strict_check_fact(authorizer, {mod, opts})
  end

  def fetch_or_strict_check_fact(authorizer, {check_module, opts}) do
    Enum.find_value(authorizer.facts, fn
      {{fact_mod, fact_opts}, result} ->
        if check_module == fact_mod &&
             Keyword.drop(fact_opts, [:access_type, :ash_field_policy?]) ==
               Keyword.drop(opts, [:access_type, :ash_field_policy?]) do
          {:ok, result}
        end

      _ ->
        nil
    end)
    |> case do
      nil ->
        if check_module.requires_original_data?(authorizer, opts) &&
             missing_original_data?(authorizer) do
          throw(
            {:error, authorizer,
             Ash.Error.Forbidden.InitialDataRequired.exception(
               source: "check: #{check_module.describe(opts)}"
             )}
          )
        else
          case check_module.strict_check(authorizer.actor, authorizer, opts) do
            {:ok, value} when is_boolean(value) or value == :unknown ->
              authorizer = %{
                authorizer
                | facts: Map.put(authorizer.facts, {check_module, opts}, value)
              }

              if value == :unknown do
                {:error, authorizer}
              else
                {:ok, value, authorizer}
              end

            {:error, error} ->
              throw({:error, authorizer, Ash.Error.to_ash_error(error)})
          end
        end

      {:ok, :unknown} ->
        {:error, authorizer}

      {:ok, value} ->
        {:ok, value, authorizer}
    end
  end

  defp missing_original_data?(%{
         changeset: %Ash.Changeset{data: %Ash.Changeset.OriginalDataNotAvailable{}}
       }) do
    true
  end

  defp missing_original_data?(_), do: false

  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.drop(fact_opts, [:access_type, :ash_field_policy?]) ==
               Keyword.drop(opts, [:access_type, :ash_field_policy?]) do
          {:ok, result}
        end

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

      :unknown ->
        :error

      value ->
        value
    end
  end

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

          {:ok, false, authorizer} ->
            {false, authorizer}

          _ ->
            {condition, authorizer}
        end

      _condition, {false, authorizer} ->
        {false, authorizer}

      condition, {expression, authorizer} ->
        case fetch_or_strict_check_fact(authorizer, condition) do
          {:ok, true, authorizer} ->
            {expression, authorizer}

          {:ok, false, authorizer} ->
            {false, authorizer}

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

  defp compile_policy_expression([], authorizer) do
    {false, authorizer}
  end

  defp compile_policy_expression(
         [%struct{condition: condition, policies: policies}],
         authorizer
       )
       when struct in [__MODULE__, Ash.Policy.FieldPolicy] do
    {condition_expression, authorizer} = condition_expression(condition, authorizer)

    case condition_expression do
      true ->
        compile_policy_expression(policies, authorizer)

      false ->
        {true, authorizer}

      nil ->
        compile_policy_expression(policies, authorizer)

      condition_expression ->
        case compile_policy_expression(policies, authorizer) do
          {true, authorizer} ->
            {true, authorizer}

          {false, authorizer} ->
            {{:not, condition_expression}, authorizer}

          {compiled_policies, authorizer} ->
            {{:or, {:and, condition_expression, compiled_policies}, {:not, condition_expression}},
             authorizer}
        end
    end
  end

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

    case condition_expression do
      true ->
        if bypass? do
          case compile_policy_expression(policies, authorizer) do
            {true, authorizer} ->
              {true, authorizer}

            {false, authorizer} ->
              {at_least_one_policy_expression, authorizer} =
                at_least_one_policy_expression(Enum.take_while(rest, &(!&1.bypass?)), authorizer)

              {rest, authorizer} = compile_policy_expression(rest, authorizer)

              {{:and, rest, at_least_one_policy_expression}, authorizer}

            {policy_expression, authorizer} ->
              {at_least_one_policy_expression, authorizer} =
                at_least_one_policy_expression(Enum.take_while(rest, &(!&1.bypass?)), authorizer)

              {rest, authorizer} = compile_policy_expression(rest, authorizer)
              {{:or, policy_expression, {:and, rest, at_least_one_policy_expression}}, authorizer}
          end
        else
          case compile_policy_expression(policies, authorizer) do
            {false, authorizer} ->
              {false, authorizer}

            {true, authorizer} ->
              compile_policy_expression(rest, authorizer)

            {policy_expression, authorizer} ->
              case compile_policy_expression(rest, authorizer) do
                {false, authorizer} ->
                  {false, authorizer}

                {true, authorizer} ->
                  {policy_expression, authorizer}

                {rest, authorizer} ->
                  {{:and, policy_expression, rest}, authorizer}
              end
          end
        end

      false ->
        compile_policy_expression(rest, authorizer)

      nil ->
        if bypass? do
          case compile_policy_expression(policies, authorizer) do
            {true, authorizer} ->
              {true, authorizer}

            {false, authorizer} ->
              {at_least_one_policy_expression, authorizer} =
                at_least_one_policy_expression(Enum.take_while(rest, &(!&1.bypass?)), authorizer)

              rest = compile_policy_expression(rest, authorizer)
              {:and, rest, at_least_one_policy_expression}

            {policy_expression, authorizer} ->
              {at_least_one_policy_expression, authorizer} =
                at_least_one_policy_expression(Enum.take_while(rest, &(!&1.bypass?)), authorizer)

              case compile_policy_expression(rest, authorizer) do
                {false, authorizer} ->
                  {policy_expression, authorizer}

                {true, authorizer} ->
                  {{:or, policy_expression, at_least_one_policy_expression}, authorizer}

                {rest, authorizer} ->
                  {{:or, policy_expression, {:and, rest, at_least_one_policy_expression}},
                   authorizer}
              end
          end
        else
          case compile_policy_expression(policies, authorizer) do
            {true, authorizer} ->
              compile_policy_expression(rest, authorizer)

            {false, authorizer} ->
              {false, authorizer}

            {policy_expression, authorizer} ->
              case compile_policy_expression(rest, authorizer) do
                {true, authorizer} ->
                  {policy_expression, authorizer}

                {false, authorizer} ->
                  {false, authorizer}

                {rest, authorizer} ->
                  {{:and, policy_expression, rest}, authorizer}
              end
          end
        end

      condition_expression ->
        if bypass? do
          {at_least_one_policy_expression, authorizer} =
            at_least_one_policy_expression(Enum.take_while(rest, &(!&1.bypass?)), authorizer)

          {condition_and_policy_expression, authorizer} =
            case compile_policy_expression(policies, authorizer) do
              {true, authorizer} ->
                {condition_expression, authorizer}

              {false, authorizer} ->
                {false, authorizer}

              {other, authorizer} ->
                {{:and, condition_expression, other}, authorizer}
            end

          case condition_and_policy_expression do
            true ->
              {true, authorizer}

            false ->
              case compile_policy_expression(rest, authorizer) do
                {true, authorizer} ->
                  {at_least_one_policy_expression, authorizer}

                {false, authorizer} ->
                  {false, authorizer}

                {rest, authorizer} ->
                  {{:and, rest, at_least_one_policy_expression}, authorizer}
              end

            condition_and_policy_expression ->
              case compile_policy_expression(rest, authorizer) do
                {true, authorizer} ->
                  {at_least_one_policy_expression, authorizer}

                {false, authorizer} ->
                  {condition_and_policy_expression, authorizer}

                {rest, authorizer} ->
                  {{:or, condition_and_policy_expression,
                    {:and, rest, at_least_one_policy_expression}}, authorizer}
              end
          end
        else
          {condition_and_policy_expression, authorizer} =
            case compile_policy_expression(policies, authorizer) do
              {true, authorizer} ->
                {condition_expression, authorizer}

              {false, authorizer} ->
                {false, authorizer}

              {policy_expression, authorizer} ->
                {{:and, condition_expression, policy_expression}, authorizer}
            end

          case condition_and_policy_expression do
            false ->
              compile_policy_expression(rest, authorizer)

            true ->
              {true, authorizer}

            condition_and_policy_expression ->
              case compile_policy_expression(rest, authorizer) do
                {true, authorizer} ->
                  {true, authorizer}

                {false, authorizer} ->
                  {condition_and_policy_expression, authorizer}

                {rest, authorizer} ->
                  {{:or, condition_and_policy_expression, rest}, authorizer}
              end
          end
        end
    end
  end

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

      {:ok, false, authorizer} ->
        {false, authorizer}

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

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

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

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

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

      {:ok, false, authorizer} ->
        {true, authorizer}

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

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

      {:ok, false, authorizer} ->
        {true, authorizer}

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

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

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

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

      {:error, authorizer} ->
        {rest, authorizer} = compile_policy_expression(rest, authorizer)

        case rest do
          true ->
            {{:not, {clause.check_module, clause.check_opts}}, authorizer}

          false ->
            {false, authorizer}

          rest ->
            {{:and, {:not, {clause.check_module, clause.check_opts}}, rest}, authorizer}
        end
    end
  end

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

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

      {:ok, false, authorizer} ->
        {false, authorizer}

      {:error, authorizer} ->
        {rest, authorizer} = compile_policy_expression(rest, authorizer)

        case rest do
          false ->
            {false, authorizer}

          true ->
            {{clause.check_module, clause.check_opts}, authorizer}

          rest ->
            {{:and, {clause.check_module, clause.check_opts}, rest}, authorizer}
        end
    end
  end
end