diff --git a/src/main/CachedEnforcer.lua b/src/main/CachedEnforcer.lua new file mode 100644 index 0000000..6e3e11a --- /dev/null +++ b/src/main/CachedEnforcer.lua @@ -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 \ No newline at end of file diff --git a/src/main/Enforcer.lua b/src/main/Enforcer.lua index caa4390..830a3ab 100644 --- a/src/main/Enforcer.lua +++ b/src/main/Enforcer.lua @@ -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, ...) diff --git a/tests/main/cached_enforcer_spec.lua b/tests/main/cached_enforcer_spec.lua new file mode 100644 index 0000000..9852fef --- /dev/null +++ b/tests/main/cached_enforcer_spec.lua @@ -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) \ No newline at end of file