diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2fdd768..a0189be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: fail-fast: false steps: @@ -32,18 +32,18 @@ jobs: nohup etcd & - name: Install and configure APISIX run: | - mkdir apisix-2.7 - wget https://downloads.apache.org/apisix/2.7/apache-apisix-2.7-src.tgz - tar zxvf apache-apisix-2.7-src.tgz -C apisix-2.7 - cd apisix-2.7 + mkdir apisix-2.15 + wget https://downloads.apache.org/apisix/2.15.3/apache-apisix-2.15.3-src.tgz + tar zxvf apache-apisix-2.15.3-src.tgz -C apisix-2.15 --strip-components=1 + cd apisix-2.15 make deps make init cd .. - name: Configure apisix-authz run: | - cp apisix-authz.lua apisix-2.7/apisix/plugins/apisix-authz.lua + cp apisix-authz.lua apisix-2.15/apisix/plugins/apisix-authz.lua # append plugins to config - cat <> apisix-2.7/conf/config.yaml + cat <> apisix-2.15/conf/config.yaml plugins: # plugin list (sorted by priority) - client-control # priority: 22000 - ext-plugin-pre-req # priority: 12000 @@ -100,7 +100,7 @@ jobs: sudo luarocks install https://raw.githubusercontent.com/casbin/lua-casbin/master/casbin-1.16.1-1.rockspec - name: Start the server run: | - cd apisix-2.7 + cd apisix-2.15 make run curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { diff --git a/README.md b/README.md index 6fef16e..7f2a447 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,42 @@ 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 Routes with Different Policies + +The plugin supports using different policies for different routes. Each unique combination of `model_path` and `policy_path` creates a separate enforcer instance. For example: + +```bash +# Route 1 with policy A +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/api/v1/*", + "plugins": { + "apisix-authz": { + "model_path": "/path/to/model.conf", + "policy_path": "/path/to/policy_v1.csv", + "username": "user" + } + }, + "upstream": {...} +}' + +# Route 2 with policy B +curl http://127.0.0.1:9080/apisix/admin/routes/2 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/api/v2/*", + "plugins": { + "apisix-authz": { + "model_path": "/path/to/model.conf", + "policy_path": "/path/to/policy_v2.csv", + "username": "user" + } + }, + "upstream": {...} +}' +``` + +Each route will use its own policy file and enforce authorization rules independently. + ## 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..e7690d2 100644 --- a/apisix-authz.lua +++ b/apisix-authz.lua @@ -16,8 +16,10 @@ local Enforcer = require("casbin") local core = require("apisix.core") local get_headers = ngx.req.get_headers --- e is the Casbin Enforcer -local CasbinEnforcer +-- Table to store multiple Casbin Enforcers keyed by configuration +local CasbinEnforcers = {} +-- Reference to the last accessed enforcer (for API operations) +local CurrentEnforcer local plugin_name = "apisix-authz" @@ -41,18 +43,25 @@ local _M = { } 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) + -- Create a unique key for this configuration + local conf_key = conf.model_path .. "|" .. conf.policy_path + + -- creates an enforcer for this configuration if it doesn't exist + if not CasbinEnforcers[conf_key] then + CasbinEnforcers[conf_key] = Enforcer:new(conf.model_path, conf.policy_path) end + local enforcer = CasbinEnforcers[conf_key] + -- Set as current enforcer for potential API operations + CurrentEnforcer = enforcer + 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 + if not enforcer:enforce(username, path, method) then return 403, {message = "Access Denied"} end else @@ -62,6 +71,10 @@ end -- subject, object, action local function addPolicy() + if not CurrentEnforcer then + return 400, {message = "No enforcer available. Send a request through a configured route first."} + end + local headers = get_headers() local type = headers["type"] @@ -74,7 +87,7 @@ local function addPolicy() return 400, {message = "Invalid policy request."} end - if CasbinEnforcer:AddPolicy(subject, object, action) then + if CurrentEnforcer:AddPolicy(subject, object, action) then return 200, {message = "Successfully added policy."} else return 400, {message = "Invalid policy request."} @@ -87,7 +100,7 @@ local function addPolicy() return 400, {message = "Invalid policy request."} end - if CasbinEnforcer:AddGroupingPolicy(user, role) then + if CurrentEnforcer:AddGroupingPolicy(user, role) then return 200, {message = "Successfully added grouping policy."} else return 400, {message = "Invalid policy request."} @@ -98,6 +111,10 @@ local function addPolicy() end local function removePolicy() + if not CurrentEnforcer then + return 400, {message = "No enforcer available. Send a request through a configured route first."} + end + local headers = get_headers() local type = headers["type"] @@ -110,7 +127,7 @@ local function removePolicy() return 400, {message = "Invalid policy request."} end - if CasbinEnforcer:RemovePolicy(subject, object, action) then + if CurrentEnforcer:RemovePolicy(subject, object, action) then return 200, {message = "Successfully removed policy."} else return 400, {message = "Invalid policy request."} @@ -123,7 +140,7 @@ local function removePolicy() return 400, {message = "Invalid policy request."} end - if CasbinEnforcer:RemoveGroupingPolicy(user, role) then + if CurrentEnforcer:RemoveGroupingPolicy(user, role) then return 200, {message = "Successfully removed grouping policy."} else return 400, {message = "Invalid policy request."} @@ -135,6 +152,10 @@ end -- subject, object, action local function hasPolicy() + if not CurrentEnforcer then + return 400, {message = "No enforcer available. Send a request through a configured route first."} + end + local headers = get_headers() local type = headers["type"] @@ -147,7 +168,7 @@ local function hasPolicy() return 400, {message = "Invalid policy request."} end - if CasbinEnforcer:HasPolicy(subject, object, action) then + if CurrentEnforcer:HasPolicy(subject, object, action) then return 200, {data = "true"} else return 200, {data = "false"} @@ -160,7 +181,7 @@ local function hasPolicy() return 400, {message = "Invalid policy request."} end - if CasbinEnforcer:HasGroupingPolicy(user, role) then + if CurrentEnforcer:HasGroupingPolicy(user, role) then return 200, {data = "true"} else return 200, {data = "false"} @@ -171,18 +192,22 @@ local function hasPolicy() end local function getPolicy() + if not CurrentEnforcer then + return 400, {message = "No enforcer available. Send a request through a configured route first."} + end + local headers = get_headers() local type = headers["type"] if type == "p" then - local policy = CasbinEnforcer:GetPolicy() + local policy = CurrentEnforcer:GetPolicy() if policy then return 200, {data = policy} else return 400 end elseif type == "g" then - local groupingPolicy = CasbinEnforcer:GetGroupingPolicy() + local groupingPolicy = CurrentEnforcer:GetGroupingPolicy() if groupingPolicy then return 200, {data = groupingPolicy} else @@ -194,8 +219,12 @@ local function getPolicy() end local function savePolicy() + if not CurrentEnforcer then + return 400, {message = "No enforcer available. Send a request through a configured route first."} + end + local _, err = pcall(function () - CasbinEnforcer:savePolicy() + CurrentEnforcer:savePolicy() end) if not err then return 200, {message = "Successfully saved policy."} @@ -231,8 +260,8 @@ function _M.api() methods = {"POST"}, uri = "/apisix/plugin/casbin/save", handler = savePolicy, - }, } + } end function _M.check_schema(conf)