diff --git a/spec/persist/saved_policy.csv b/spec/persist/saved_policy.csv new file mode 100644 index 0000000..8479f3c --- /dev/null +++ b/spec/persist/saved_policy.csv @@ -0,0 +1,5 @@ +p, alice, data1, read +p, bob, data2, write +p, data2_admin, data2, read +p, data2_admin, data2, write +g, alice, data2_admin diff --git a/spec/rbac/role_manager_spec.lua b/spec/rbac/role_manager_spec.lua new file mode 100644 index 0000000..12a25c1 --- /dev/null +++ b/spec/rbac/role_manager_spec.lua @@ -0,0 +1,276 @@ +--Copyright 2021 The casbin Authors. All Rights Reserved. +-- +--Licensed under the Apache License, Version 2.0 (the "License"); +--you may not use this file except in compliance with the License. +--You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +--Unless required by applicable law or agreed to in writing, software +--distributed under the License is distributed on an "AS IS" BASIS, +--WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +--See the License for the specific language governing permissions and +--limitations under the License. + +local role_manager_module = require("src.rbac.DefaultRoleManager") +local util_module = require("src.util.Util") + +-- test function for testing the matching function functionality in Role Manager +-- true if n1=n2 or n1 matches the pattern of n2 or n2 matches the pattern of n1 +-- "?" character needed before any pattern +function testMatchingFunc(...) + local args = {...} + local n1 = args[1] + local n2 = args[2] + if n1 == n2 then + return true + elseif string.sub(n1, 1, 1) == "?" then + if string.find(n2, string.sub(n1, 2)) then + return true + end + elseif string.sub(n2, 1, 1) == "?" then + if string.find(n1, string.sub(n2, 2)) then + return true + end + end + return false +end + +describe("DefaultRoleManager tests", function () + it("test role", function () + local rm = DefaultRoleManager:new(10) + rm:addLink("u1", "g1") + rm:addLink("u2", "g1") + rm:addLink("u3", "g2") + rm:addLink("u4", "g2") + rm:addLink("u4", "g3") + rm:addLink("g1", "g3") + --[[ + # Current role inheritance tree: + # g3 g2 + # / \ / \ + # g1 u4 u3 + # / \ + # u1 u2 + ]] + assert.is.True(rm:hasLink("u1", "g1")) + assert.is.False(rm:hasLink("u1", "g2")) + assert.is.True(rm:hasLink("u1", "g3")) + assert.is.True(rm:hasLink("u2", "g1")) + assert.is.False(rm:hasLink("u2", "g2")) + assert.is.True(rm:hasLink("u2", "g3")) + assert.is.False(rm:hasLink("u3", "g1")) + assert.is.True(rm:hasLink("u3", "g2")) + assert.is.False(rm:hasLink("u3", "g3")) + assert.is.False(rm:hasLink("u4", "g1")) + assert.is.True(rm:hasLink("u4", "g2")) + assert.is.True(rm:hasLink("u4", "g3")) + + assert.are.same(rm:getRoles("u1"),{"g1"}) + assert.are.same(rm:getRoles("u2"),{"g1"}) + assert.are.same(rm:getRoles("u3"),{"g2"}) + assert.are.same(rm:getRoles("u4"),{"g2", "g3"}) + assert.are.same(rm:getRoles("g1"),{"g3"}) + assert.are.same(rm:getRoles("g2"),{}) + assert.are.same(rm:getRoles("g3"),{}) + + assert.are.same(rm:getUsers("u1"),{}) + assert.are.same(rm:getUsers("u2"),{}) + assert.are.same(rm:getUsers("u3"),{}) + assert.are.same(rm:getUsers("u4"),{}) + assert.is.True(Util.areTablesSame(rm:getUsers("g1"),{"u2", "u1"})) + assert.is.True(Util.areTablesSame(rm:getUsers("g2"),{"u3", "u4"})) + assert.is.True(Util.areTablesSame(rm:getUsers("g3"),{"u4", "g1"})) + + rm:deleteLink("g1", "g3") + rm:deleteLink("u4", "g2") + + --[[ + # Current role inheritance tree after deleting the links: + # g3 g2 + # \ \ + # g1 u4 u3 + # / \ + # u1 u2 + ]] + + assert.is.True(rm:hasLink("u1", "g1")) + assert.is.False(rm:hasLink("u1", "g2")) + assert.is.False(rm:hasLink("u1", "g3")) + assert.is.True(rm:hasLink("u2", "g1")) + assert.is.False(rm:hasLink("u2", "g2")) + assert.is.False(rm:hasLink("u2", "g3")) + assert.is.False(rm:hasLink("u3", "g1")) + assert.is.True(rm:hasLink("u3", "g2")) + assert.is.False(rm:hasLink("u3", "g3")) + assert.is.False(rm:hasLink("u4", "g1")) + assert.is.False(rm:hasLink("u4", "g2")) + assert.is.True(rm:hasLink("u4", "g3")) + + assert.are.same(rm:getRoles("u1"),{"g1"}) + assert.are.same(rm:getRoles("u2"),{"g1"}) + assert.are.same(rm:getRoles("u3"),{"g2"}) + assert.are.same(rm:getRoles("u4"),{"g3"}) + assert.are.same(rm:getRoles("g1"),{}) + assert.are.same(rm:getRoles("g2"),{}) + assert.are.same(rm:getRoles("g3"),{}) + end) + + it("test domain role", function () + local rm = DefaultRoleManager:new(10) + rm:addLink("u1", "g1", "domain1") + rm:addLink("u2", "g1", "domain1") + rm:addLink("u3", "admin", "domain2") + rm:addLink("u4", "admin", "domain2") + rm:addLink("u4", "admin", "domain1") + rm:addLink("g1", "admin", "domain1") + --[[ + # Current role inheritance tree: + # domain1:admin domain2:admin + # / \ / \ + # domain1:g1 u4 u3 + # / \ + # u1 u2 + ]] + assert.is.True(rm:hasLink("u1", "g1", "domain1")) + assert.is.False(rm:hasLink("u1", "g1", "domain2")) + assert.is.True(rm:hasLink("u1", "admin", "domain1")) + assert.is.False(rm:hasLink("u1", "admin", "domain2")) + + assert.is.True(rm:hasLink("u2", "g1", "domain1")) + assert.is.False(rm:hasLink("u2", "g1", "domain2")) + assert.is.True(rm:hasLink("u2", "admin", "domain1")) + assert.is.False(rm:hasLink("u2", "admin", "domain2")) + + assert.is.False(rm:hasLink("u3", "g1", "domain1")) + assert.is.False(rm:hasLink("u3", "g1", "domain2")) + assert.is.False(rm:hasLink("u3", "admin", "domain1")) + assert.is.True(rm:hasLink("u3", "admin", "domain2")) + + assert.is.False(rm:hasLink("u4", "g1", "domain1")) + assert.is.False(rm:hasLink("u4", "g1", "domain2")) + assert.is.True(rm:hasLink("u4", "admin", "domain1")) + assert.is.True(rm:hasLink("u4", "admin", "domain2")) + end) + + it("test clear", function () + local rm = DefaultRoleManager:new(10) + rm:addLink("u1", "g1") + rm:addLink("u2", "g1") + rm:addLink("u3", "g2") + rm:addLink("u4", "g2") + rm:addLink("u4", "g3") + rm:addLink("g1", "g3") + --[[ + # Current role inheritance tree: + # g3 g2 + # / \ / \ + # g1 u4 u3 + # / \ + # u1 u2 + ]] + rm:clear() + + -- All data is cleared. + -- No role inheritance now. + + assert.is.False(rm:hasLink("u1", "g1")) + assert.is.False(rm:hasLink("u1", "g2")) + assert.is.False(rm:hasLink("u1", "g3")) + assert.is.False(rm:hasLink("u2", "g1")) + assert.is.False(rm:hasLink("u2", "g2")) + assert.is.False(rm:hasLink("u2", "g3")) + assert.is.False(rm:hasLink("u3", "g1")) + assert.is.False(rm:hasLink("u3", "g2")) + assert.is.False(rm:hasLink("u3", "g3")) + assert.is.False(rm:hasLink("u4", "g1")) + assert.is.False(rm:hasLink("u4", "g2")) + assert.is.False(rm:hasLink("u4", "g3")) + end) + + it("test matchingFunc", function () + local rm = DefaultRoleManager:new(10, testMatchingFunc) + rm:addLink("u1", "g1") + rm:addLink("u3", "g2") + rm:addLink("u3", "g3") + rm:addLink("?^[+-]?u%d+$", "g2") + + assert.is.True(rm:hasLink("u1", "g1")) + assert.is.True(rm:hasLink("u1", "g2")) + assert.is.False(rm:hasLink("u1", "g3")) + + assert.is.False(rm:hasLink("u2", "g1")) + assert.is.True(rm:hasLink("u2", "g2")) + assert.is.False(rm:hasLink("u2", "g3")) + + assert.is.False(rm:hasLink("u3", "g1")) + assert.is.True(rm:hasLink("u3", "g2")) + assert.is.True(rm:hasLink("u3", "g3")) + end) + + it("test one to many", function () + local rm = DefaultRoleManager:new(10, testMatchingFunc) + rm:addLink("u1", "?^[+-]?g%d+$") + assert.is.True(rm:hasLink("u1", "g1")) + assert.is.True(rm:hasLink("u1", "g2")) + assert.is.False(rm:hasLink("u2", "g1")) + assert.is.False(rm:hasLink("u2", "g2")) + end) + + it("test many to one", function () + local rm = DefaultRoleManager:new(10, testMatchingFunc) + rm:addLink("?^[+-]?u%d+$", "g1") + assert.is.True(rm:hasLink("u1", "g1")) + assert.is.True(rm:hasLink("u2", "g1")) + assert.is.False(rm:hasLink("u1", "g2")) + assert.is.False(rm:hasLink("u2", "g2")) + end) + + it("test matching function order", function () + local rm = DefaultRoleManager:new(10, testMatchingFunc) + + rm:addLink("?^[+-]?g%d+$", "root") + rm:addLink("u1", "g1") + assert.is.True(rm:hasLink("u1", "root")) + + rm:clear() + + rm:addLink("u1", "g1") + rm:addLink("?^[+-]?g%d+$", "root") + assert.is.True(rm:hasLink("u1", "root")) + + rm:clear() + + rm:addLink("u1", "?^[+-]?g%d+$") + rm:addLink("g1", "root") + assert.is.True(rm:hasLink("u1", "root")) + + rm:clear() + + rm:addLink("g1", "root") + rm:addLink("u1", "?^[+-]?g%d+$") + assert.is.True(rm:hasLink("u1", "root")) + end) + + it("test toString", function () + local rm = DefaultRoleManager:new(10) + rm:addLink("u1", "g1") + rm:addLink("u2", "g1") + rm:addLink("u3", "g2") + rm:addLink("u4", "g2") + rm:addLink("u4", "g3") + rm:addLink("g1", "g3") + --[[ + # Current role inheritance tree: + # g3 g2 + # / \ / \ + # g1 u4 u3 + # / \ + # u1 u2 + ]] + + assert.are.same(rm:createRole("u1"):toString(), "u1 < g1") + assert.are.same(rm:createRole("g1"):toString(), "g1 < g3") + assert.are.same(rm:createRole("u4"):toString(), "u4 < g2, g3") + end) +end) diff --git a/src/model/Assertion.lua b/src/model/Assertion.lua index 6143f7c..5906782 100644 --- a/src/model/Assertion.lua +++ b/src/model/Assertion.lua @@ -36,22 +36,22 @@ end function Assertion:buildRoleLinks(rm) self.RM = rm local count = 0 - for i = 1, string.len(value) do - if string.sub(value,i,i) == '_' then + for i = 1, string.len(self.value) do + if string.sub(self.value,i,i) == '_' then count = count + 1 end end for _, rule in pairs(self.policy) do if count < 2 then - error("the number of \"_\" in role definition should be at least 2") + error("the number of '_' in role definition should be at least 2") end if #rule < count then error("grouping policy elements do not meet role definition") end - local name1, name2 = nil + local name1, name2 local domain = {} for i, string in pairs(rule) do if i > count then break end @@ -73,22 +73,22 @@ end function Assertion:buildIncrementalRoleLinks(rm, op, rules) self.RM = rm local count = 0 - for i = 1, string.len(value) do - if string.sub(value,i,i) == '_' then + for i = 1, string.len(self.value) do + if string.sub(self.value,i,i) == '_' then count = count + 1 end end for _, rule in pairs(rules) do if count < 2 then - error("the number of \"_\" in role definition should be at least 2") + error("the number of '_' in role definition should be at least 2") end if #rule < count then error("grouping policy elements do not meet role definition") end - local name1, name2 = nil + local name1, name2 local domain = {} for i, string in pairs(rule) do if i > count then break end diff --git a/src/rbac/DefaultRoleManager.lua b/src/rbac/DefaultRoleManager.lua index 705609a..0e0a37f 100644 --- a/src/rbac/DefaultRoleManager.lua +++ b/src/rbac/DefaultRoleManager.lua @@ -12,11 +12,10 @@ --See the License for the specific language governing permissions and --limitations under the License. -DefaultRoleManager = { - defaultDomain = 'casbin::default', - allDomains, - maxHierarchyLevel, +require "src/rbac/Role" +DefaultRoleManager = { + maxHierarchyLevel = 0 } --[[ @@ -35,57 +34,44 @@ DefaultRoleManager = { * @param matchingFunc a matcher for supporting pattern in g * @param domainMatchingFunc a matcher for supporting domain pattern in g ]] -function DefaultRoleManager:DefaultRoleManager(maxHierarchyLevel, matchingFunc, domainMatchingFunc) - allDomains = {} - self.maxHierarchyLevel = maxHierarchyLevel - self.matchingFunc = matchingFunc - self.domainMatchingFunc = domainMatchingFunc -end - -function domainName(...) - arg = {...} - if #arg == 0 then - return defaultDomain - else - return arg[1] - end +function DefaultRoleManager:new(maxHierarchyLevel, matchingFunc, domainMatchingFunc) + local o = {} + setmetatable(o, self) + self.__index = self + o.allRoles = {} + o.maxHierarchyLevel = maxHierarchyLevel + o.matchingFunc = matchingFunc + o.domainMatchingFunc = domainMatchingFunc + return o end ---[[ - * Build temporary roles when a domain matching function is defined, else the domain or default - * roles. - * - * @param domain eventual domain - * @return matched domain roles or domain roles -]] -function getMatchingDomainRoles(...) - domain = {...} - if domainMatchingFunc ~= nil then - return generateTempRoles(domainName(domain)) +function DefaultRoleManager:hasRole(name) + if self.matchingFunc then + for key, _ in pairs(self.allRoles) do + if self.matchingFunc(name, key) then + return true + end + end else - return getOrCreateDomainRoles(domainName(domain)) + if self.allRoles[name] then + return true + end end -end - -function generateTempRoles(...) + return false end -function getPatternMatchedDomainNames(domain) - -end - -function createTempRolesForDomain(allRoles, domainName) +function DefaultRoleManager:createRole(name) + if not self.allRoles[name] then + self.allRoles[name] = Role:new(name) + end + return self.allRoles[name] end -- * clear clears all stored data and resets the role manager to the initial state. function DefaultRoleManager:clear() - -end - -function getOrCreateDomainRoles(domain) - + self.allRoles = {} end --[[ @@ -93,7 +79,36 @@ end * inherits role: name2. domain is a prefix to the roles. ]] function DefaultRoleManager:addLink(name1, name2, ...) + local domain = {...} + if #domain == 1 then + name1 = domain[1] .. "::" .. name1 + name2 = domain[1] .. "::" .. name2 + elseif #domain > 1 then + error("domain should be only 1 parameter") + else + domain = nil + end + local role1 = self:createRole(name1) + local role2 = self:createRole(name2) + role1:addRole(role2) + + if self.matchingFunc then + for key, role in pairs(self.allRoles) do + if self.matchingFunc(key, name1) and name1 ~= key then + self.allRoles[key]:addRole(role1) + end + if self.matchingFunc(key, name2) and name2 ~= key then + self.allRoles[name2]:addRole(role) + end + if self.matchingFunc(name1, key) and name1 ~= key then + self.allRoles[key]:addRole(role1) + end + if self.matchingFunc(name2, key) and name2 ~= key then + self.allRoles[name2]:addRole(role) + end + end + end end --[[ @@ -101,31 +116,119 @@ end * does not inherit role: name2 any more. domain is a prefix to the roles. ]] function DefaultRoleManager:deleteLink(name1, name2, ...) + local domain = {...} + if #domain == 1 then + name1 = domain[1] .. "::" .. name1 + name2 = domain[1] .. "::" .. name2 + elseif #domain > 1 then + error("domain should be only 1 parameter") + else + domain = nil + end + + if not (self:hasRole(name1)) or not (self:hasRole(name2)) then + error("name1 or name2 does not exist") + end + local role1 = self:createRole(name1) + local role2 = self:createRole(name2) + role1:deleteRole(role2) end --- * hasLink determines whether role: name1 inherits role: name2. domain is a prefix to the roles. +-- hasLink determines whether role: name1 inherits role: name2. domain is a prefix to the roles. function DefaultRoleManager:hasLink(name1, name2, ...) + local domain = {...} + if #domain == 1 then + name1 = domain[1] .. "::" .. name1 + name2 = domain[1] .. "::" .. name2 + elseif #domain > 1 then + error("domain should be only 1 parameter") + else + domain = nil + end -end + if name1 == name2 then + return true + end + + if not (self:hasRole(name1)) or not (self:hasRole(name2)) then + return false + end -function isValidDomainOrThrow(...) + if self.matchingFunc then + for key, role in pairs(self.allRoles) do + if self.matchingFunc(name1, key) and (role:hasRole(name2, self.maxHierarchyLevel, self.matchingFunc)) then + return true + end + end + return false + else + local role1 = self:createRole(name1) + return role1:hasRole(name2, self.maxHierarchyLevel) + end end -- * getRoles gets the roles that a subject inherits. domain is a prefix to the roles. function DefaultRoleManager:getRoles(name, ...) + local domain = {...} + if #domain == 1 then + name = domain[1] .. "::" .. name + elseif #domain > 1 then + error("domain should be only 1 parameter") + else + domain = nil + end + + if not self:hasRole(name) then + return {} + end + + local roles = self:createRole(name):getRoles() + + if domain then + for key, value in pairs(roles) do + roles[key] = string.sub(value, #domain[1]+3) + end + end + return roles end --- * getUsers gets the users that inherits a subject. +-- getUsers gets the users that inherits a subject. function DefaultRoleManager:getUsers(name, ...) + local domain = {...} + if #domain == 1 then + name = domain[1] .. "::" .. name + elseif #domain > 1 then + error("domain should be only 1 parameter") + else + domain = nil + end + local names = {} + for _, role in pairs(self.allRoles) do + if role:hasDirectRole(name) then + if domain then + table.insert(names, string.sub(role.name, #domain[1]+3)) + else + table.insert(names, role.name) + end + end + end + + return names end --- * printRoles prints all the roles to log. +-- printRoles prints all the roles to log. function DefaultRoleManager:printRoles(name, ...) + local lines = {} + for _, role in pairs(self.allRoles) do + local text = role:toString() + if text then table.insert(lines, text) end + end + -- TODO: add logger here end -return DefaultRoleManager \ No newline at end of file +return DefaultRoleManager diff --git a/src/rbac/DomainRoles.lua b/src/rbac/DomainRoles.lua deleted file mode 100644 index 93ba4f6..0000000 --- a/src/rbac/DomainRoles.lua +++ /dev/null @@ -1,91 +0,0 @@ ---Copyright 2021 The casbin Authors. All Rights Reserved. --- ---Licensed under the Apache License, Version 2.0 (the "License"); ---you may not use this file except in compliance with the License. ---You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- ---Unless required by applicable law or agreed to in writing, software ---distributed under the License is distributed on an "AS IS" BASIS, ---WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ---See the License for the specific language governing permissions and ---limitations under the License. - -require "src/rbac/Role" - ---[[ - * Represents all roles in a domain -]] -DomainRoles = {} - --- private var -local roles = {} - -function DomainRoles:hasRole(name, matchingFunc) - if matchingFunc ~= nil then - for r, n in pairs(roles) do - local f = loadstring (tostring(matchingFunc)) - if f() then - return true - end - end - return false - else - if roles[name] ~= nil then - return true - else - return false - end - end -end - -function DomainRoles:createRole(name, matchingFunc) - local flag = 0 - for k, v in pairs(roles) do - if k == name then - role = roles[name] - flag = 1 - end - end - if flag == 0 then - roles[name] = Role:new(name) - role = roles[name] - end - - if matchingFunc ~= nil then - for k, v in pairs(roles) do - if isRoleEntryMatchExists(k, name, matchingFunc) then - role:addRole(k) - end - end - end - - return role -end - -function isRoleEntryMatchExists(roleEntryKey, name, matchingFunc) - if roleEntryKey == name then - return false - end - local f = loadstring (tostring(matchingFunc)) -- matchingFunc.test(name, roleEntryKey) - if f() then - return true - end - - return false -end - -function DomainRoles:getOrCreate(name) - for k, v in pairs(roles) do - if k == name then - return roles[k] - end - end - - roles[name] = Role:new(name) - return roles[name] -end - - -return DomainRoles \ No newline at end of file diff --git a/src/rbac/Role.lua b/src/rbac/Role.lua index 7d06259..370ae3c 100644 --- a/src/rbac/Role.lua +++ b/src/rbac/Role.lua @@ -19,14 +19,14 @@ function Role:new(name) local o = {} setmetatable(o, self) self.__index = self - self.name = name - self.roles = {} + o.name = name + o.roles = {} return o end function Role:addRole(role) - for i=1, #self.roles do - if self.roles[i].name == role.name then + for _, r in pairs(self.roles) do + if r.name == role.name then return end end @@ -35,32 +35,46 @@ function Role:addRole(role) end function Role:deleteRole(role) - toRemove = {} - for i=1, #self.roles do - if self.roles[i].name == role.name then - table.remove(self.roles, i) + for k, r in pairs(self.roles) do + if r.name == role.name then + table.remove(self.roles, k) end end end -function Role:hasRole(name, hierarchyLevel) +function Role:hasRole(name, hierarchyLevel, matchingFunc) if self.name == name then return true end + if self:hasDirectRole(name, matchingFunc) then + return true + end + if hierarchyLevel <= 0 then return false end - res = false - for i=1, #self.roles do - res = res or self.roles[i]:hasRole(name, hierarchyLevel - 1) + + for _, r in pairs(self.roles) do + if r:hasRole(name, hierarchyLevel - 1, matchingFunc) then + return true + end end - return res + + return false end -function Role:hasDirectRole(name) - for i=1, #self.roles do - if self.roles[i] == name then - return true +function Role:hasDirectRole(name, matchingFunc) + if matchingFunc then + for _, r in pairs(self.roles) do + if matchingFunc(name, r.name) then + return true + end + end + else + for _, r in pairs(self.roles) do + if r.name == name then + return true + end end end @@ -68,16 +82,25 @@ function Role:hasDirectRole(name) end function Role:toString() - - return + local names = "" + names = self.name .. " < " + + for k, r in pairs(self.roles) do + if k==1 then + names = names .. r.name + else + names = names .. ", " .. r.name + end + end + return names end function Role:getRoles() - names = {} - for i=1, #self.roles do - table.insert(names, self.roles[i]) + local names = {} + for _, r in pairs(self.roles) do + table.insert(names, r.name) end return names end -return Role \ No newline at end of file +return Role diff --git a/src/util/Util.lua b/src/util/Util.lua index fba844a..28870c4 100644 --- a/src/util/Util.lua +++ b/src/util/Util.lua @@ -182,4 +182,31 @@ function Util.isInstance(o, parent) return false end +-- Searches if all values in a table are present in the other table regardless of order +function Util.areTablesSame(a, b) + local c = {} + for _, v in pairs(a) do + if c[v] then + c[v] = c[v] + 1 + else + c[v] = 1 + end + end + + for _, v in pairs(b) do + if c[v] then + c[v] = c[v] - 1 + if c[v] == 0 then + c[v] = nil + end + else + return false + end + end + for _, v in pairs(c) do + return false + end + return true +end + return Util