This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-lua-apisix-authz.git


The following commit(s) were added to refs/heads/master by this push:
     new 734ee43  fix: create a Casbin Enforcer per model/policy pair
734ee43 is described below

commit 734ee43a17c5ef7b12b771748fafda4dc340141d
Author: Yang Luo <[email protected]>
AuthorDate: Fri Aug 7 00:01:41 2026 +0800

    fix: create a Casbin Enforcer per model/policy pair
---
 README.md        | 49 ++++++++++++++++++++++++++++++
 apisix-authz.lua | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++-----
 2 files changed, 133 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md
index 6fef16e..4b1fbe7 100644
--- a/README.md
+++ b/README.md
@@ -94,6 +94,55 @@ curl -X PUT \
 
 As per the current configuration, if the request is authorized, the execution 
will proceed normally. While if it is not authorized, it will return "Access 
Denied" message with the 403 exit code and stop any further execution.
 
+## Multiple models and policies
+
+Different routes can use different models and policies. The plugin keeps one 
Casbin Enforcer per `model_path` + `policy_path` pair, so a route is always 
enforced against the model and policy it was configured with:
+
+```
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: 
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "uri": "/service1/*",
+    "plugins": {
+        "apisix-authz": {
+            "model_path": "/path/to/service1_model.conf",
+            "policy_path": "/path/to/service1_policy.csv",
+            "username": "user"
+        }
+    },
+    "upstream": {"type": "roundrobin", "nodes": {"service1.com": 1}}
+}'
+
+curl http://127.0.0.1:9080/apisix/admin/routes/2 -H 'X-API-KEY: 
edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "uri": "/service2/*",
+    "plugins": {
+        "apisix-authz": {
+            "model_path": "/path/to/service2_model.conf",
+            "policy_path": "/path/to/service2_policy.csv",
+            "username": "user"
+        }
+    },
+    "upstream": {"type": "roundrobin", "nodes": {"service2.com": 1}}
+}'
+```
+
+Routes that share the same `model_path` and `policy_path` share a single 
Enforcer, so a policy added at runtime through the plugin API is visible to all 
of them.
+
+The policy management APIs (`/apisix/plugin/casbin/add`, `remove`, `has`, 
`get` and `save`) act on one Enforcer, selected with the `model_path` and 
`policy_path` headers:
+
+```
+curl -i -X POST \
+  --url http://127.0.0.1:9080/apisix/plugin/casbin/add \
+  --header 'model_path: /path/to/service1_model.conf' \
+  --header 'policy_path: /path/to/service1_policy.csv' \
+  --header 'type: p' \
+  --header 'subject: alice' \
+  --header 'object: /service1/data' \
+  --header 'action: GET'
+```
+
+These two headers may be omitted only when a single Enforcer exists. Otherwise 
the request is answered with a 400 status code, since the Enforcer to act on 
would be ambiguous.
+
 ## Development
 
 If you wish to customize this according to your scenario, you can do so by 
customizing the apisix-authz.lua file from your plugins directory.
diff --git a/apisix-authz.lua b/apisix-authz.lua
index 4bfa46e..8217538 100644
--- a/apisix-authz.lua
+++ b/apisix-authz.lua
@@ -16,11 +16,16 @@ local Enforcer = require("casbin")
 local core = require("apisix.core")
 local get_headers = ngx.req.get_headers
 
--- e is the Casbin Enforcer
-local CasbinEnforcer
+-- the Casbin Enforcers, keyed by the model/policy paths of a route's 
configuration,
+-- so that routes configured with different models or policies get their own 
Enforcer
+local CasbinEnforcers = {}
 
 local plugin_name = "apisix-authz"
 
+local enforcer_not_found_message = "No Casbin Enforcer found for this request, 
"
+                                .. "please set the model_path and policy_path 
headers "
+                                .. "to the ones of the route you want to 
manage."
+
 local schema = {
     type = "object",
     properties = {
@@ -40,10 +45,57 @@ local _M = {
     schema = schema
 }
 
+local function enforcerKey(model_path, policy_path)
+    return model_path .. "\0" .. policy_path
+end
+
+-- returns the Enforcer for this configuration, creating it on the first 
request
+-- that uses this model/policy pair
+local function getEnforcer(conf)
+    local key = enforcerKey(conf.model_path, conf.policy_path)
+
+    if not CasbinEnforcers[key] then
+        local ok, e = pcall(Enforcer.new, Enforcer, conf.model_path, 
conf.policy_path)
+        if not ok then
+            core.log.error("Failed to create the Casbin Enforcer for model: "
+                            .. conf.model_path .. ", policy: " .. 
conf.policy_path
+                            .. ", error: " .. tostring(e))
+            return nil
+        end
+        CasbinEnforcers[key] = e
+    end
+
+    return CasbinEnforcers[key]
+end
+
+-- returns the Enforcer a policy management API request refers to. The model 
and
+-- policy paths are taken from the "model_path" and "policy_path" headers, 
which
+-- may be omitted when a single Enforcer has been created.
+local function getApiEnforcer()
+    local headers = get_headers()
+    local model_path = headers["model_path"]
+    local policy_path = headers["policy_path"]
+
+    if model_path and policy_path then
+        return CasbinEnforcers[enforcerKey(model_path, policy_path)]
+    end
+
+    local enforcer, count = nil, 0
+    for _, e in pairs(CasbinEnforcers) do
+        enforcer = e
+        count = count + 1
+    end
+    if count == 1 then
+        return enforcer
+    end
+
+    return nil
+end
+
 function _M.rewrite(conf)
-    -- creates an enforcer when request sent for the first time
+    local CasbinEnforcer = getEnforcer(conf)
     if not CasbinEnforcer then
-        CasbinEnforcer = Enforcer:new(conf.model_path, conf.policy_path)
+        return 403, {message = "Access Denied"}
     end
 
     local path = ngx.var.request_uri
@@ -62,6 +114,11 @@ end
 
 -- subject, object, action
 local function addPolicy()
+    local CasbinEnforcer = getApiEnforcer()
+    if not CasbinEnforcer then
+        return 400, {message = enforcer_not_found_message}
+    end
+
     local headers = get_headers()
     local type = headers["type"]
 
@@ -98,6 +155,11 @@ local function addPolicy()
 end
 
 local function removePolicy()
+    local CasbinEnforcer = getApiEnforcer()
+    if not CasbinEnforcer then
+        return 400, {message = enforcer_not_found_message}
+    end
+
     local headers = get_headers()
     local type = headers["type"]
 
@@ -135,6 +197,11 @@ end
 
 -- subject, object, action
 local function hasPolicy()
+    local CasbinEnforcer = getApiEnforcer()
+    if not CasbinEnforcer then
+        return 400, {message = enforcer_not_found_message}
+    end
+
     local headers = get_headers()
     local type = headers["type"]
 
@@ -171,6 +238,11 @@ local function hasPolicy()
 end
 
 local function getPolicy()
+    local CasbinEnforcer = getApiEnforcer()
+    if not CasbinEnforcer then
+        return 400, {message = enforcer_not_found_message}
+    end
+
     local headers = get_headers()
     local type = headers["type"]
 
@@ -194,13 +266,18 @@ local function getPolicy()
 end
 
 local function savePolicy()
-    local _, err = pcall(function ()
+    local CasbinEnforcer = getApiEnforcer()
+    if not CasbinEnforcer then
+        return 400, {message = enforcer_not_found_message}
+    end
+
+    local ok, err = pcall(function ()
         CasbinEnforcer:savePolicy()
     end)
-    if not err then
+    if ok then
         return 200, {message = "Successfully saved policy."}
     else
-        core.log.error("Save Policy error: " .. err)
+        core.log.error("Save Policy error: " .. tostring(err))
         return 400, {message = "Failed to save policy, see logs."}
     end
 end


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to