-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Description
Background
Hi, we have been developing casbin-authz plugin for APISIX based on Lua Casbin which is the Lua implementation of the Casbin library. Casbin is an authorization library which supports access control models like ACL, RBAC and ABAC. casbin-authz is a plugin for APISIX that enables authorization based on Casbin. The initial implementation is at apisix-authz.
Implementation and Usage
This is what we have developed till now:
local Enforcer = require("casbin")
local core = require("apisix.core")
local get_headers = ngx.req.get_headers
local CasbinEnforcer
local plugin_name = "apisix-authz"
local schema = {
type = "object",
properties = {
model_path = { type = "string" },
policy_path = { type = "string" },
username = { type = "string"}
},
required = {"model_path", "policy_path", "username"},
additionalProperties = false
}
local _M = {
version = 0.1,
priority = 2560,
type = 'auth',
name = plugin_name,
schema = schema
}
function _M.rewrite(conf)
-- creates an enforcer when request sent for the first time
if not CasbinEnforcer then
CasbinEnforcer = Enforcer:new(conf.model_path, conf.policy_path)
end
local path = ngx.var.request_uri
local method = ngx.var.request_method
local username = get_headers()[conf.username]
if not username then
username = "anonymous"
end
if path and method and username then
if not CasbinEnforcer:enforce(username, path, method) then
return 403, {message = "Access Denied"}
end
else
return 403, {message = "Access Denied"}
end
end
local function addPolicy()
local headers = get_headers()
local type = headers["type"]
if type == "p" then
local subject = headers["subject"]
local object = headers["object"]
local action = headers["action"]
if not subject or not object or not action then
return 400, {message = "Invalid policy request."}
end
if CasbinEnforcer:AddPolicy(subject, object, action) then
return 200, {message = "Successfully added policy."}
else
return 400, {message = "Invalid policy request."}
end
elseif type == "g" then
local user = headers["user"]
local role = headers["role"]
if not user or not role then
return 400, {message = "Invalid policy request."}
end
if CasbinEnforcer:AddGroupingPolicy(user, role) then
return 200, {message = "Successfully added grouping policy."}
else
return 400, {message = "Invalid policy request."}
end
else
return 400, {message = "Invalid policy type."}
end
end
local function removePolicy()
local headers = get_headers()
local type = headers["type"]
if type == "p" then
local subject = headers["subject"]
local object = headers["object"]
local action = headers["action"]
if not subject or not object or not action then
return 400, {message = "Invalid policy request."}
end
if CasbinEnforcer:RemovePolicy(subject, object, action) then
return 200, {message = "Successfully removed policy."}
else
return 400, {message = "Invalid policy request."}
end
elseif type == "g" then
local user = headers["user"]
local role = headers["role"]
if not user or not role then
return 400, {message = "Invalid policy request."}
end
if CasbinEnforcer:RemoveGroupingPolicy(user, role) then
return 200, {message = "Successfully removed grouping policy."}
else
return 400, {message = "Invalid policy request."}
end
else
return 400, {message = "Invalid policy type."}
end
end
-- subject, object, action
local function hasPolicy()
local headers = get_headers()
local type = headers["type"]
if type == "p" then
local subject = headers["subject"]
local object = headers["object"]
local action = headers["action"]
if not subject or not object or not action then
return 400, {message = "Invalid policy request."}
end
if CasbinEnforcer:HasPolicy(subject, object, action) then
return 200, {data = "true"}
else
return 200, {data = "false"}
end
elseif type == "g" then
local user = headers["user"]
local role = headers["role"]
if not user or not role then
return 400, {message = "Invalid policy request."}
end
if CasbinEnforcer:HasGroupingPolicy(user, role) then
return 200, {data = "true"}
else
return 200, {data = "false"}
end
else
return 400, {message = "Invalid policy type."}
end
end
local function getPolicy()
local headers = get_headers()
local type = headers["type"]
if type == "p" then
local policy = CasbinEnforcer:GetPolicy()
if policy then
return 200, {data = policy}
else
return 400
end
elseif type == "g" then
local groupingPolicy = CasbinEnforcer:GetGroupingPolicy()
if groupingPolicy then
return 200, {data = groupingPolicy}
else
return 400
end
else
return 400, {message = "Invalid policy type."}
end
end
local function savePolicy()
local _, err = pcall(function ()
CasbinEnforcer:savePolicy()
end)
if not err then
return 200, {message = "Successfully saved policy."}
else
core.log.error("Save Policy error: " .. err)
return 400, {message = "Failed to save policy, see logs."}
end
end
function _M.api()
return {
{
methods = {"POST"},
uri = "/apisix/plugin/casbin/add",
handler = addPolicy,
},
{
methods = {"POST"},
uri = "/apisix/plugin/casbin/remove",
handler = removePolicy,
},
{
methods = {"GET"},
uri = "/apisix/plugin/casbin/has",
handler = hasPolicy,
},
{
methods = {"GET"},
uri = "/apisix/plugin/casbin/get",
handler = getPolicy,
},
{
methods = {"POST"},
uri = "/apisix/plugin/casbin/save",
handler = savePolicy,
},
}
end
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
return _MThe user can send send a request to configure the plugin on a route by:
curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"uri": "/*",
"plugins": {
"apisix-authz": {
"model_path": "/path/to/authz_model.conf",
"policy_path": "/path/to/authz_policy.csv",
"username": "user"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"example.com": 1
}
},
"host": "example.com"
}'This will use the model in model_path and policy in policy_path of the configuration to create a Casbin Enforcer when run for the first time. (Example model file and policy file here). The plugin checks whether the username (as passed in header), the object (the path URL) and the HTTP request method are authorized or not. If the username header is not present, it assumes it to be anonymous whose permissions can be set in the model/policy files. If such request is authorized, it will proceed normally as it would and if not it would return a 403 code (for now).
It also features an API to get the policies, configure the policies and save all of them again(if updated) to the policy file path. The API is in initial stage and may support more functions as needed.
What do you think about this? Will this be helpful? If so, I can start with an initial PR.