Skip to content

Proposal: Addition of an authorization plugin #4674

@rushitote

Description

@rushitote

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 _M

The 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions