Skip to content

Commit

Permalink
feat: Added CachedEnforcer and tests for it (#62)
Browse files Browse the repository at this point in the history
Signed-off-by: Rushikesh Tote <[email protected]>
  • Loading branch information
rushitote authored Jun 27, 2021
1 parent c243902 commit 90b47cf
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 0 deletions.
82 changes: 82 additions & 0 deletions src/main/CachedEnforcer.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
--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.main.Enforcer")

-- CachedEnforcer wraps Enforcer and provides decision cache
CachedEnforcer = {}
setmetatable(CachedEnforcer, Enforcer)

-- Creates a cached enforcer via file or DB.
function CachedEnforcer:new(model, adapter)
local e = Enforcer:new(model, adapter)
self.__index = self
setmetatable(e, self)
e.cacheEnabled = true
e.m = {}
return e
end

-- enableCache determines whether to enable cache on Enforce(). When enableCache is enabled, cached result (true | false) will be returned for previous decisions.
function CachedEnforcer:enableCache(enabled)
if enabled then
self.cacheEnabled = true
else
self.cacheEnabled = false
end
end

-- enforce decides whether a "subject" can access a "object" with the operation "action", input parameters are usually: (sub, obj, act).
-- if rvals is not string , ingore the cache
function CachedEnforcer:enforce(...)
if not self.cacheEnabled then
return Enforcer.enforce(self, ...)
end

local rvals = {...}
local key = ""
for _, rval in pairs(rvals) do
if type(rval) == "string" then
key = key .. rval .. "$$"
else
return Enforcer.enforce(self, ...)
end
end

local res, ok = self:getCachedResult(key)
if ok then
return res
end

res = Enforcer.enforce(self, ...)

self:setCachedResult(key, res)
return res
end

function CachedEnforcer:getCachedResult(key)
if self.m[key] ~= nil then
return self.m[key], true
end

return nil, false
end

function CachedEnforcer:setCachedResult(key, res)
self.m[key] = res
end

function CachedEnforcer:invalidateCache()
self.m = {}
end
1 change: 1 addition & 0 deletions src/main/Enforcer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require("src.main.ManagementEnforcer")
-- Enforcer = ManagementEnforcer + RBAC API.
Enforcer = {}
setmetatable(Enforcer, ManagementEnforcer)
Enforcer.__index = Enforcer

-- GetRolesForUser gets the roles that a user has.
function Enforcer:GetRolesForUser(name, ...)
Expand Down
59 changes: 59 additions & 0 deletions tests/main/cached_enforcer_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
--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 cached_enforcer_module = require("src.main.CachedEnforcer")
local path = os.getenv("PWD") or io.popen("cd"):read()

describe("Cached Enforcer tests", function ()
it("Test Cache", function ()
local model = path .. "/examples/basic_model.conf"
local policy = path .. "/examples/basic_policy.csv"

local e = CachedEnforcer:new(model, policy)
-- The cache is enabled by default for a new CachedEnforcer.

assert.is.True(e:enforce("alice", "data1", "read"))
assert.is.False(e:enforce("alice", "data1", "write"))
assert.is.False(e:enforce("alice", "data2", "read"))
assert.is.False(e:enforce("alice", "data2", "write"))

-- The cache is enabled, so even if we remove a policy rule, the decision
-- for ("alice", "data1", "read") will still be true, as it uses the cached result.
e:RemovePolicy("alice", "data1", "read")

assert.is.True(e:enforce("alice", "data1", "read"))
assert.is.False(e:enforce("alice", "data1", "write"))
assert.is.False(e:enforce("alice", "data2", "read"))
assert.is.False(e:enforce("alice", "data2", "write"))

-- Now we invalidate the cache, then all first-coming Enforce() has to be evaluated in real-time.
-- The decision for ("alice", "data1", "read") will be false now.
e:invalidateCache()

assert.is.False(e:enforce("alice", "data1", "read"))
assert.is.False(e:enforce("alice", "data1", "write"))
assert.is.False(e:enforce("alice", "data2", "read"))
assert.is.False(e:enforce("alice", "data2", "write"))

e:AddPolicy("alice", "data1", "read")

-- Disabling cache skips the cache data and generates result from Enforcer
e:enableCache(false)

assert.is.True(e:enforce("alice", "data1", "read"))
assert.is.False(e:enforce("alice", "data1", "write"))
assert.is.False(e:enforce("alice", "data2", "read"))
assert.is.False(e:enforce("alice", "data2", "write"))
end)
end)

0 comments on commit 90b47cf

Please sign in to comment.