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