Skip to content

Instantly share code, notes, and snippets.

@polvorin
Created October 19, 2022 16:16
Show Gist options
  • Save polvorin/9f49fd92c1462f433e5ed2e6d047882c to your computer and use it in GitHub Desktop.
Save polvorin/9f49fd92c1462f433e5ed2e6d047882c to your computer and use it in GitHub Desktop.
defmodule Rule do
# Values in our language are typed.
@type value() :: {:string, binary()}
| {:integer, integer()}
| {:bool, boolean()}
| {:list, list(value())}
@type attribute_source() :: :subject | :resource | :action
# Terminals, either literal values or values retrived from the environment
@type terminal() :: {:literal, value()} | {:attribute, {attribute_source(), binary()}}
# Expression, either a terminal, or an operation applied on a list of expressions.
@type expr() :: {:term, terminal()} | {:op, {op(), [expr()]}}
# Operators we support. Besides booleans, can add functions like list length, aritmethic expressions, etc.
@type op() :: :eq |:member |:lt |:not |:and |:length |:add
# Rules are boolean expressions
def eval(req, expr) do
case eval_expr(req, expr) do
{:bool, value} -> value
_ -> false
end
end
defp eval_expr(req, {:term, t}), do: eval_term(req, t)
defp eval_expr(req, {:op, {operator, arguments}}) do
eval_op(operator, Enum.map(arguments, &(eval_expr(req, &1))))
end
defp eval_term(_, {:literal, v}), do: v
defp eval_term(req, {:attribute, {source, name}}) do
case req do
%{^source => %{^name => v}} -> v
#TODO: introduce a ':nil' type, or error here.
end
end
# We allow <= between strings or between integers
defp eval_op(:lt, [{t, v1}, {t, v2}]) when t == :string or t == :integer do
{:bool, v1 <= v2}
end
defp eval_op(:lt, [{_t1, _}, {_t2, _}]) do
{:bool, false} #or raise exception, bad rule can't compare values of types t1 and t2
end
defp eval_op(:eq, [v1,v2]), do: {:bool, v1 == v2}
defp eval_op(:member, [el, {:list, v2}]) do
{:bool, Enum.member?(v2, el)}
end
defp eval_op(:member, [_, {_t, _}]) do
{:bool, false} #or raise exception, not a list.
end
defp eval_op(:and, args), do: {:bool, Enum.all?(args, fn x -> x == {:bool, true} end)}
defp eval_op(:not, [{:bool, v}]), do: {:bool, not v}
defp eval_op(:length, [{:list, l}]), do: {:integer, Enum.count(l)}
defp eval_op(:add, [{:integer, i1}, {:integer, i2}]), do: {:integer, i1 + i2}
end
## Example rules and evaluation
## Subject must have role = auditor, and subject level must be < resource level
rule = {:op, {:and, [{:op, {:eq, [{:term, {:attribute, {:subject, :role}}},
{:term, {:literal, {:string, "auditor"}}}]}},
{:op, {:lt, [{:term, {:attribute, {:subject, :level}}},
{:term, {:attribute, {:resource, :level}}}]}}]}}
true = Rule.eval(%{subject: %{level: {:integer, 2}, role: {:string, "auditor"}},
resource: %{level: {:integer, 4}}},
rule)
false = Rule.eval(%{subject: %{level: {:integer, 2}, role: {:string, "member"}},
resource: %{level: {:integer, 4}}},
rule)
false = Rule.eval(%{subject: %{level: {:integer, 5}, role: {:string, "auditor"}},
resource: %{level: {:integer, 4}}},
rule)
## resource' name must be a member of subject' "allowed" list
rule = {:op, {:member, [{:term, {:attribute, {:resource, :name}}},
{:term, {:attribute, {:subject, :allow_list}}}]}}
true = Rule.eval(%{subject: %{allow_list: {:list, [{:string, "a"}, {:string, "b"}]}},
resource: %{name: {:string, "a"}}},
rule)
false = Rule.eval(%{subject: %{allow_list: {:list, [{:string, "a"}, {:string, "b"}]}},
resource: %{name: {:string, "z"}}},
rule)
# Always true
true = Rule.eval(%{}, {:term, {:literal, {:bool, true}}})
false = Rule.eval(%{}, {:term, {:literal, {:bool, false}}})
## nonsense rule. length of list 'a' in resource less than subject' number + 1
rule = {:op, {:lt, [{:op, {:length, [{:term, {:attribute, {:resource, :a}}}]}},
{:op, {:add, [{:term, {:attribute, {:subject, :number}}},
{:term, {:literal, {:integer, 1}}}]}}]}}
true = Rule.eval(%{subject: %{number: {:integer, 3}},
resource: %{a: {:list, [{:string, "a"}, {:string, "b"}, {:string, "c"}]}}},
rule)
false = Rule.eval(%{subject: %{number: {:integer, 1}},
resource: %{a: {:list, [{:string, "a"}, {:string, "b"}, {:string, "c"}]}}},
rule)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment