Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Addition of an authorization plugin #4674

Closed
rushitote opened this issue Jul 26, 2021 · 10 comments · Fixed by #4710
Closed

Proposal: Addition of an authorization plugin #4674

rushitote opened this issue Jul 26, 2021 · 10 comments · Fixed by #4710

Comments

@rushitote
Copy link
Contributor

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.

@spacewander
Copy link
Member

LGTM. There are three minor issues:

  1. local plugin_name = "apisix-authz" should be local plugin_name = "casbin-authz"
  2. we don't need type = 'auth' as it doesn't interact with consumers
  3. Use underline style name instead of camel case, like get_policy but not getPolicy

@spacewander
Copy link
Member

We already have authz-keycloak, what about using name authz-casbin?

@hsluoyz
Copy link

hsluoyz commented Jul 27, 2021

@spacewander thanks! authz-casbin would be good.

@tokers
Copy link
Contributor

tokers commented Jul 27, 2021

Just one question, So if people want to use this plugin when they deploy their Apache APISIX cluster on Kuberentes, we may have to prepare an image which contains the policy file and the mode configuration. Or, we should support to mount such files when they run the container, say, we can expose such configurations items in the helm chart.

In addition, is it convenient to support to write literal policy and mode config just in the plugin config?

@rushitote
Copy link
Contributor Author

LGTM. There are three minor issues:

  1. local plugin_name = "apisix-authz" should be local plugin_name = "casbin-authz"
  2. we don't need type = 'auth' as it doesn't interact with consumers
  3. Use underline style name instead of camel case, like get_policy but not getPolicy
  1. Yes, I will change that to authz-casbin.
  2. Right, I will remove it then and change the running phase to access.
  3. Yes, I will fix this and make style more consistent.

Thanks!

@rushitote
Copy link
Contributor Author

@tokers

I had the same thought when developing this, so in its current stage they would have to have their model/policy files across all nodes. But, I am not very sure which option could be better.

We can add support to write literal policies/models directly into config but I am not sure if this will be convenient if the policy file is large (say 1000 policies or so), still this would be a good option. Should we add this as an alternative to using files?

Thanks!

@spacewander
Copy link
Member

@rushitote
What about using plugin-metadata to set up the policy: https://github.com/apache/apisix/blob/master/docs/en/latest/admin-api.md#plugin-metadata? The plugin metadata is stored in etcd

@tokers
Copy link
Contributor

tokers commented Jul 27, 2021

@rushitote
What about using plugin-metadata to set up the policy: https://github.com/apache/apisix/blob/master/docs/en/latest/admin-api.md#plugin-metadata? The plugin metadata is stored in etcd

+1, that'd would be better.

@rushitote
Copy link
Contributor Author

@spacewander

That is a great option, we could use that!

@rushitote
Copy link
Contributor Author

Created a PR at #4710.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants