Created
October 19, 2022 16:16
-
-
Save polvorin/9f49fd92c1462f433e5ed2e6d047882c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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