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]