lib/actors/security/acl/rules/acl_evaluator.ex

defmodule Actors.Security.Acl.Rules.AclEvaluator do
  @moduledoc """
  `AclEvaluator`is responsible for evaluating whether a request can be accepted
  or not based on the informed Access Control List policy.
  """
  alias Actors.Security.Acl.Policy

  alias Eigr.Functions.Protocol.Actors.{
    Actor,
    ActorId
  }

  alias Eigr.Functions.Protocol.InvocationRequest

  @doc """
  Applies a policy on a request by evaluating whether the request should pass or not.

  ## Example

      iex> Actors.Security.Acl.Rules.AclEvaluator.eval(
      ...> %Actors.Security.Acl.Policy{
      ...>   name: "the-police",
      ...>   type: :allow,
      ...>   actors: ["*"],
      ...>   actions: ["get"],
      ...>   actor_systems: ["*"]
      ...> },
      ...> %Eigr.Functions.Protocol.InvocationRequest{
      ...>    actor: %Eigr.Functions.Protocol.Actors.Actor{id: %Eigr.Functions.Protocol.Actors.ActorId{name: "joe"}},
      ...>    action_name: "get",
      ...>    caller: %Eigr.Functions.Protocol.Actors.ActorId{name: "robert", system: "actor-system"}
      ...> }
      ...> )
      true
  """
  @spec eval(Policy.t(), InvocationRequest.t(), Keyword.t()) :: boolean()
  def eval(policy, invocation, opts \\ [])

  def eval(
        %Policy{
          name: name,
          type: type,
          actors: actors,
          actions: actions,
          actor_systems: actor_systems
        },
        invocation,
        opts
      ) do
    actors = normalize_list_input(actors)
    actions = normalize_list_input(actions)
    actor_systems = normalize_list_input(actor_systems)

    eval_options = [name: name, invocation: invocation]
    opts = Keyword.merge(opts, eval_options)

    do_evaluation(type, actor_systems, actors, actions, opts)
  end

  defp do_evaluation(type, :all, :all, :all, _opts) when is_atom(type) and type == :allow,
    do: true

  defp do_evaluation(type, :all, :all, :all, _opts) when is_atom(type) and type == :deny,
    do: false

  defp do_evaluation(type, actor_systems, actors, actions, opts) do
    %InvocationRequest{
      actor: %Actor{} = _actor,
      caller: %ActorId{name: from_actor_name, system: from_system},
      action_name: action
    } = _invocation = Keyword.get(opts, :invocation)

    has_system? = match_op?(actor_systems, from_system)
    has_actor_name? = match_op?(actors, from_actor_name)
    has_action? = match_op?(actions, action)

    case type do
      :allow ->
        has_system? && has_actor_name? && has_action?

      :deny ->
        !(has_system? && has_actor_name? && has_action?)
    end
  end

  defp match_op?(:all, _elem), do: true

  defp match_op?(list, elem), do: Enum.member?(list, elem)

  defp normalize_list_input(list) do
    if Enum.member?(list, "*") do
      :all
    else
      list
    end
  end
end