--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements.  See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License.  You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--

local casbin          = require("casbin")
local core            = require("apisix.core")
local plugin          = require("apisix.plugin")

local plugin_name = "authz-casbin"

local schema = {
    type = "object",
    properties = {
        model_path = { type = "string" },
        policy_path = { type = "string" },
        model = { type = "string" },
        policy = { type = "string" },
        username = { type = "string"}
    },
    oneOf = {
        {required = {"model_path", "policy_path", "username"}},
        {required = {"model", "policy", "username"}}
    },
}

local metadata_schema = {
    type = "object",
    properties = {
        model = {type = "string"},
        policy = {type = "string"},
    },
    required = {"model", "policy"},
}

local _M = {
    version = 0.1,
    priority = 2560,
    name = plugin_name,
    schema = schema,
    metadata_schema = metadata_schema
}

function _M.check_schema(conf, schema_type)
    if schema_type == core.schema.TYPE_METADATA then
        return core.schema.check(metadata_schema, conf)
    end
    local ok, err = core.schema.check(schema, conf)
    if ok then
        return true
    else
        local metadata = plugin.plugin_metadata(plugin_name)
        if metadata and metadata.value and conf.username then
            return true
        end
    end
    return false, err
end

local casbin_enforcer

local function new_enforcer_if_need(conf)
    if conf.model_path and conf.policy_path then
        local model_path = conf.model_path
        local policy_path = conf.policy_path
        if not conf.casbin_enforcer then
            conf.casbin_enforcer = casbin:new(model_path, policy_path)
        end
        return true
    end

    if conf.model and conf.policy then
        local model = conf.model
        local policy = conf.policy
        if not conf.casbin_enforcer then
            conf.casbin_enforcer = casbin:newEnforcerFromText(model, policy)
        end
        return true
    end

    local metadata = plugin.plugin_metadata(plugin_name)
    if not (metadata and metadata.value.model and metadata.value.policy) then
        return nil, "not enough configuration to create enforcer"
    end

    local modifiedIndex = metadata.modifiedIndex
    if not casbin_enforcer or casbin_enforcer.modifiedIndex ~= modifiedIndex then
        local model = metadata.value.model
        local policy = metadata.value.policy
        casbin_enforcer = casbin:newEnforcerFromText(model, policy)
        casbin_enforcer.modifiedIndex = modifiedIndex
    end
    return true
end


function _M.rewrite(conf, ctx)
    -- creates an enforcer when request sent for the first time
    local ok, err = new_enforcer_if_need(conf)
    if not ok then
        core.log.error(err)
        return 503
    end

    local path = ctx.var.uri
    local method = ctx.var.method
    local headers = core.request.headers(ctx)
    local username = headers[conf.username] or "anonymous"

    if conf.casbin_enforcer then
        if not conf.casbin_enforcer:enforce(username, path, method) then
            return 403, {message = "Access Denied"}
        end
    else
        if not casbin_enforcer:enforce(username, path, method) then
            return 403, {message = "Access Denied"}
        end
    end
end



return _M
