diff --git a/lib/acx/enforcer.ex b/lib/acx/enforcer.ex index 5f520b1..6040432 100644 --- a/lib/acx/enforcer.ex +++ b/lib/acx/enforcer.ex @@ -38,7 +38,7 @@ defmodule Acx.Enforcer do # conflicts with sone built-in function names? env = role_groups - |> Enum.map(fn {name, g} -> {name, RoleGroup.stub(g)} end) + |> Enum.map(fn {name, g} -> {name, RoleGroup.stub_2(g)} end) |> Map.new() |> Map.merge(init_env()) @@ -301,7 +301,28 @@ defmodule Acx.Enforcer do %{ enforcer | role_groups: %{groups | mapping_name => group}, - env: %{env | mapping_name => RoleGroup.stub(group)} + env: %{env | mapping_name => RoleGroup.stub_2(group)} + } + end + end + + def add_mapping_policy( + %__MODULE__{role_groups: groups, env: env} = enforcer, + {mapping_name, role1, role2, dom} + ) when is_atom(mapping_name) and is_binary(role1) and is_binary(role2) and is_binary(dom) do + case Map.get(groups, mapping_name) do + nil -> + {:error, "mapping name not found: `#{mapping_name}`"} + + group -> + group = + group + |> RoleGroup.add_inheritance({role1, role2 <> dom}) + + %{ + enforcer | + role_groups: %{groups | mapping_name => group}, + env: %{env | mapping_name => RoleGroup.stub_3(group)} } end end @@ -319,6 +340,19 @@ defmodule Acx.Enforcer do end end + def add_mapping_policy!( + %__MODULE__{} = enforcer, + {mapping_name, role1, role2, dom} + ) when is_atom(mapping_name) and is_binary(role1) and is_binary(role2) and is_binary(dom) do + case add_mapping_policy(enforcer, {mapping_name, role1, role2, dom}) do + {:error, reason} -> + raise ArgumentError, message: reason + + enforcer -> + enforcer + end + end + @doc """ Loads mapping policies from a csv file and adds them to the enforcer. @@ -342,7 +376,10 @@ defmodule Acx.Enforcer do |> Enum.map(&String.split(&1, ~r{,\s*})) |> Enum.map(fn [key | attrs] -> [String.to_atom(key) | attrs] end) |> Enum.filter(fn [key | _] -> Model.has_role_mapping?(m, key) end) - |> Enum.map(fn [name, r1, r2] -> {name, r1, r2} end) + |> Enum.map(fn + [name, r1, r2] -> {name, r1, r2} + [name, r1, r2, d] -> {name, r1, r2, d} + end) |> Enum.reduce(enforcer, &add_mapping_policy!(&2, &1)) end diff --git a/lib/acx/enforcer_server.ex b/lib/acx/enforcer_server.ex index 891558f..699a8ae 100644 --- a/lib/acx/enforcer_server.ex +++ b/lib/acx/enforcer_server.ex @@ -88,6 +88,13 @@ defmodule Acx.EnforcerServer do ) end + def add_mapping_policy(ename, {mapping_name, role1, role2, dom}) do + GenServer.call( + via_tuple(ename), + {:add_mapping_policy, {mapping_name, role1, role2 <> dom}} + ) + end + @doc """ Loads mapping policies from a csv file and adds them to the enforcer. diff --git a/lib/acx/internal/role_group.ex b/lib/acx/internal/role_group.ex index 075bbb4..795a751 100644 --- a/lib/acx/internal/role_group.ex +++ b/lib/acx/internal/role_group.ex @@ -86,7 +86,7 @@ defmodule Acx.Internal.RoleGroup do false """ @spec add_inheritance(t(), {role_type(), role_type()}) :: t() - def add_inheritance(%__MODULE__{role_graph: g} = group, {r1, r2})do + def add_inheritance(%__MODULE__{role_graph: g} = group, {r1, r2}) do %{group | role_graph: g |> Digraph.add_edge({r1, r2})} end @@ -122,15 +122,28 @@ defmodule Acx.Internal.RoleGroup do iex> g = RoleGroup.new(:g) ...> g = g |> RoleGroup.add_inheritance({"admin", "member"}) - ...> f = g |> RoleGroup.stub + ...> f = g |> RoleGroup.stub_2 ...> false = f.("member", "admin") ...> false = f.(1, 2) ...> f.("admin", "member") true + ...> g = g |> RoleGroup.add_inheritance({"admin", "memberdomain"}) + ...> f = g |> RoleGroup.stub_3 + ...> false = f.("member", "admin", "dom") + ...> f.("admin", "member", "domain") + true """ - @spec stub(t()) :: function() - def stub(%__MODULE__{} = group) do - fn arg1, arg2 -> group |> inherit_from?(arg1, arg2) end + def stub_2(%__MODULE__{} = group) do + fn + arg1, arg2 -> + group |> inherit_from?(arg1, arg2) + end end + def stub_3(%__MODULE__{} = group) do + fn + arg1, arg2, arg3 -> + group |> inherit_from?(arg1, arg2 <> arg3) + end + end end diff --git a/lib/acx/model.ex b/lib/acx/model.ex index e4e9792..910f9a3 100644 --- a/lib/acx/model.ex +++ b/lib/acx/model.ex @@ -524,9 +524,15 @@ defmodule Acx.Model do # A valid role definition should be `{key, "_,_"}` in which # `key` must be an atom. defp check_role_definition([]), do: :ok + defp check_role_definition([{_key, "_,_"} | rest]) do check_role_definition(rest) end + + defp check_role_definition([{_key, "_,_,_"} | rest]) do + check_role_definition(rest) + end + defp check_role_definition([{key, val} | _]) do {:error, "invalid role definition: `#{key}=#{val}`"} end diff --git a/test/data/rbac_domain.conf b/test/data/rbac_domain.conf new file mode 100644 index 0000000..57c3721 --- /dev/null +++ b/test/data/rbac_domain.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, dom, obj, act + +[policy_definition] +p = sub, dom, obj, act + +[role_definition] +g = _, _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/test/data/rbac_domain.csv b/test/data/rbac_domain.csv new file mode 100644 index 0000000..b7a571f --- /dev/null +++ b/test/data/rbac_domain.csv @@ -0,0 +1,10 @@ +p, admin, domain1, data1, read +p, admin, domain1, data1, write +p, admin, domain2, data2, read +p, admin, domain2, data2, write +p, user, domain3, data2, read + +g, alice, admin, domain1 +g, alice, admin, domain2 +g, bob, admin, domain2 +g, bob, user, domain3 diff --git a/test/enforcer/acl_model_test.exs b/test/enforcer/acl_model_test.exs index a04f558..f76b574 100644 --- a/test/enforcer/acl_model_test.exs +++ b/test/enforcer/acl_model_test.exs @@ -3,8 +3,8 @@ defmodule Acx.Enforcer.AclModelTest do alias Acx.Model.Policy alias Acx.Enforcer - @cfile "../data/acl.conf" |> Path.expand(__DIR__) - @pfile "../data/acl.csv" |> Path.expand(__DIR__) + @cfile "../data/acl.conf" |> Path.expand(__DIR__) + @pfile "../data/acl.csv" |> Path.expand(__DIR__) setup do {:ok, e} = Enforcer.init(@cfile) diff --git a/test/enforcer/acl_restful_model_test.exs b/test/enforcer/acl_restful_model_test.exs index d1c80ba..699ea19 100644 --- a/test/enforcer/acl_restful_model_test.exs +++ b/test/enforcer/acl_restful_model_test.exs @@ -2,8 +2,8 @@ defmodule Acx.Enforcer.AclRestfulModelTest do use ExUnit.Case, async: true alias Acx.Enforcer - @cfile "../data/acl_restful.conf" |> Path.expand(__DIR__) - @pfile "../data/acl_restful.csv" |> Path.expand(__DIR__) + @cfile "../data/acl_restful.conf" |> Path.expand(__DIR__) + @pfile "../data/acl_restful.csv" |> Path.expand(__DIR__) setup do {:ok, e} = Enforcer.init(@cfile) diff --git a/test/enforcer/rbac_domain_model_test.exs b/test/enforcer/rbac_domain_model_test.exs new file mode 100644 index 0000000..189d0c8 --- /dev/null +++ b/test/enforcer/rbac_domain_model_test.exs @@ -0,0 +1,52 @@ +defmodule Acx.Enforcer.RbacDomainModelTest do + use ExUnit.Case, async: true + alias Acx.Enforcer + + @cfile "../data/rbac_domain.conf" |> Path.expand(__DIR__) + @pfile "../data/rbac_domain.csv" |> Path.expand(__DIR__) + + setup do + {:ok, e} = Enforcer.init(@cfile) + + e = + e + |> Enforcer.load_policies!(@pfile) + |> Enforcer.load_mapping_policies!(@pfile) + + {:ok, e: e} + end + + describe "allow?/2" do + @test_cases [ + {["alice", "domain1", "data1", "read"], true}, + {["alice", "domain1", "data1", "write"], true}, + {["alice", "domain2", "data2", "read"], true}, + {["alice", "domain2", "data2", "write"], true}, + {["alice", "domain2", "data2", "no_existing"], false}, + {["alice", "domain2", "no_existing", "read"], false}, + {["alice", "domain3", "data2", "read"], false}, + + {["bob", "domain1", "data1", "read"], false}, + {["bob", "domain1", "data1", "write"], false}, + {["bob", "domain2", "data2", "read"], true}, + {["bob", "domain2", "data2", "write"], true}, + {["bob", "domain2", "data2", "no_existing"], false}, + {["bob", "domain2", "no_existing", "read"], false}, + {["bob", "domain3", "data2", "read"], true}, + + {["peter", "domain1", "data1", "read"], false}, + {["peter", "domain1", "data1", "write"], false}, + {["peter", "domain2", "data2", "read"], false}, + {["peter", "domain2", "data2", "write"], false}, + {["peter", "domain2", "data2", "no_existing"], false}, + {["peter", "domain2", "no_existing", "read"], false}, + {["peter", "domain3", "data2", "read"], false}, + ] + + Enum.each(@test_cases, fn {req, res} -> + test "response `#{res}` for request #{inspect(req)}", %{e: e} do + assert e |> Enforcer.allow?(unquote(req)) === unquote(res) + end + end) + end +end diff --git a/test/enforcer/rbac_model_test.exs b/test/enforcer/rbac_model_test.exs index 56f3288..2baa4fb 100644 --- a/test/enforcer/rbac_model_test.exs +++ b/test/enforcer/rbac_model_test.exs @@ -2,8 +2,8 @@ defmodule Acx.Enforcer.RbacModelTest do use ExUnit.Case, async: true alias Acx.Enforcer - @cfile "../data/rbac.conf" |> Path.expand(__DIR__) - @pfile "../data/rbac.csv" |> Path.expand(__DIR__) + @cfile "../data/rbac.conf" |> Path.expand(__DIR__) + @pfile "../data/rbac.csv" |> Path.expand(__DIR__) setup do {:ok, e} = Enforcer.init(@cfile)