Skip to content

Commit

Permalink
feat: RBAC domain model (casbin#10)
Browse files Browse the repository at this point in the history
* rbac domain model

* test: adding tests for rbac domain
  • Loading branch information
Zat42 authored Mar 18, 2022
1 parent 5b0e701 commit daafa79
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 14 deletions.
43 changes: 40 additions & 3 deletions lib/acx/enforcer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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

Expand Down
7 changes: 7 additions & 0 deletions lib/acx/enforcer_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 18 additions & 5 deletions lib/acx/internal/role_group.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
6 changes: 6 additions & 0 deletions lib/acx/model.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions test/data/rbac_domain.conf
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions test/data/rbac_domain.csv
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions test/enforcer/acl_model_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions test/enforcer/acl_restful_model_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
52 changes: 52 additions & 0 deletions test/enforcer/rbac_domain_model_test.exs
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions test/enforcer/rbac_model_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit daafa79

Please sign in to comment.