- Implements Apache APISIX packaging for Cloudron platform. - Includes Dockerfile, CloudronManifest.json, and start.sh. - Configured to use Cloudron's etcd addon. 🤖 Generated with Gemini CLI Co-Authored-By: Gemini <noreply@google.com>
493 lines
15 KiB
Lua
493 lines
15 KiB
Lua
--
|
|
-- 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 core = require("apisix.core")
|
|
local consumer = require("apisix.consumer")
|
|
local json = require("apisix.core.json")
|
|
local sleep = core.sleep
|
|
local ngx_re = require("ngx.re")
|
|
local http = require("resty.http")
|
|
local ngx = ngx
|
|
local rawget = rawget
|
|
local rawset = rawset
|
|
local setmetatable = setmetatable
|
|
local type = type
|
|
local string = string
|
|
local req_read_body = ngx.req.read_body
|
|
local req_get_body_data = ngx.req.get_body_data
|
|
|
|
local plugin_name = "wolf-rbac"
|
|
|
|
|
|
local schema = {
|
|
type = "object",
|
|
properties = {
|
|
appid = {
|
|
type = "string",
|
|
default = "unset"
|
|
},
|
|
server = {
|
|
type = "string",
|
|
default = "http://127.0.0.1:12180"
|
|
},
|
|
header_prefix = {
|
|
type = "string",
|
|
default = "X-"
|
|
},
|
|
}
|
|
}
|
|
|
|
local _M = {
|
|
version = 0.1,
|
|
priority = 2555,
|
|
type = 'auth',
|
|
name = plugin_name,
|
|
schema = schema,
|
|
}
|
|
|
|
|
|
local token_version = 'V1'
|
|
local function create_rbac_token(appid, wolf_token)
|
|
return token_version .. "#" .. appid .. "#" .. wolf_token
|
|
end
|
|
|
|
local function fail_response(message, init_values)
|
|
local response = init_values or {}
|
|
response.message = message
|
|
return response
|
|
end
|
|
|
|
local function success_response(message, init_values)
|
|
local response = init_values or {}
|
|
response.message = message
|
|
return response
|
|
end
|
|
|
|
local function parse_rbac_token(rbac_token)
|
|
local res, err = ngx_re.split(rbac_token, "#", nil, nil, 3)
|
|
if not res then
|
|
return nil, err
|
|
end
|
|
|
|
if #res ~= 3 or res[1] ~= token_version then
|
|
return nil, 'invalid rbac token: version'
|
|
end
|
|
local appid = res[2]
|
|
local wolf_token = res[3]
|
|
|
|
return {appid = appid, wolf_token = wolf_token}
|
|
end
|
|
|
|
local function new_headers()
|
|
local t = {}
|
|
local lt = {}
|
|
local _mt = {
|
|
__index = function(t, k)
|
|
return rawget(lt, string.lower(k))
|
|
end,
|
|
__newindex = function(t, k, v)
|
|
rawset(t, k, v)
|
|
rawset(lt, string.lower(k), v)
|
|
end,
|
|
}
|
|
return setmetatable(t, _mt)
|
|
end
|
|
|
|
-- timeout in ms
|
|
local function http_req(method, uri, body, myheaders, timeout)
|
|
if not myheaders then
|
|
myheaders = new_headers()
|
|
end
|
|
|
|
local httpc = http.new()
|
|
if timeout then
|
|
httpc:set_timeout(timeout)
|
|
end
|
|
|
|
local res, err = httpc:request_uri(uri, {
|
|
method = method,
|
|
headers = myheaders,
|
|
body = body,
|
|
ssl_verify = false
|
|
})
|
|
|
|
if not res then
|
|
core.log.error("FAIL REQUEST [ ",core.json.delay_encode(
|
|
{method = method, uri = uri, body = body, headers = myheaders}),
|
|
" ] failed! res is nil, err:", err)
|
|
return nil, err
|
|
end
|
|
|
|
return res
|
|
end
|
|
|
|
local function http_get(uri, myheaders, timeout)
|
|
return http_req("GET", uri, nil, myheaders, timeout)
|
|
end
|
|
|
|
|
|
function _M.check_schema(conf)
|
|
local check = {"server"}
|
|
core.utils.check_https(check, conf, plugin_name)
|
|
core.log.info("input conf: ", core.json.delay_encode(conf))
|
|
|
|
local ok, err = core.schema.check(schema, conf)
|
|
if not ok then
|
|
return false, err
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
local function fetch_rbac_token(ctx)
|
|
if ctx.var.arg_rbac_token then
|
|
return ngx.unescape_uri(ctx.var.arg_rbac_token)
|
|
end
|
|
|
|
if ctx.var.http_authorization then
|
|
return ctx.var.http_authorization
|
|
end
|
|
|
|
if ctx.var.http_x_rbac_token then
|
|
return ctx.var.http_x_rbac_token
|
|
end
|
|
|
|
return ctx.var['cookie_x-rbac-token']
|
|
end
|
|
|
|
|
|
local function check_url_permission(server, appid, action, resName, client_ip, wolf_token)
|
|
local retry_max = 3
|
|
local errmsg
|
|
local userInfo
|
|
local res
|
|
local err
|
|
local access_check_url = server .. "/wolf/rbac/access_check"
|
|
local headers = new_headers()
|
|
headers["x-rbac-token"] = wolf_token
|
|
headers["Content-Type"] = "application/json; charset=utf-8"
|
|
local args = { appID = appid, resName = resName, action = action, clientIP = client_ip}
|
|
local url = access_check_url .. "?" .. ngx.encode_args(args)
|
|
local timeout = 1000 * 10
|
|
|
|
for i = 1, retry_max do
|
|
-- TODO: read apisix info.
|
|
res, err = http_get(url, headers, timeout)
|
|
if err then
|
|
break
|
|
else
|
|
core.log.info("check permission request:", url, ", status:", res.status,
|
|
",body:", core.json.delay_encode(res.body))
|
|
if res.status < 500 then
|
|
break
|
|
else
|
|
core.log.info("request [curl -v ", url, "] failed! status:", res.status)
|
|
if i < retry_max then
|
|
sleep(0.1)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if err then
|
|
core.log.error("fail request: ", url, ", err:", err)
|
|
return {
|
|
status = 500,
|
|
err = "request to wolf-server failed, err:" .. err
|
|
}
|
|
end
|
|
|
|
if res.status ~= 200 and res.status >= 500 then
|
|
return {
|
|
status = 500,
|
|
err = 'request to wolf-server failed, status:' .. res.status
|
|
}
|
|
end
|
|
|
|
local body, err = json.decode(res.body)
|
|
if not body then
|
|
errmsg = 'check permission failed! parse response json failed!'
|
|
core.log.error( "json.decode(", res.body, ") failed! err:", err)
|
|
return {status = res.status, err = errmsg}
|
|
else
|
|
if body.data then
|
|
userInfo = body.data.userInfo
|
|
end
|
|
errmsg = body.reason
|
|
return {status = res.status, err = errmsg, userInfo = userInfo}
|
|
end
|
|
end
|
|
|
|
|
|
function _M.rewrite(conf, ctx)
|
|
local url = ctx.var.uri
|
|
local action = ctx.var.request_method
|
|
local client_ip = ctx.var.http_x_real_ip or core.request.get_ip(ctx)
|
|
local perm_item = {action = action, url = url, clientIP = client_ip}
|
|
core.log.info("hit wolf-rbac rewrite")
|
|
|
|
local rbac_token = fetch_rbac_token(ctx)
|
|
if rbac_token == nil then
|
|
core.log.info("no permission to access ",
|
|
core.json.delay_encode(perm_item), ", need login!")
|
|
return 401, fail_response("Missing rbac token in request")
|
|
end
|
|
|
|
local tokenInfo, err = parse_rbac_token(rbac_token)
|
|
core.log.info("token info: ", core.json.delay_encode(tokenInfo),
|
|
", err: ", err)
|
|
if err then
|
|
return 401, fail_response('invalid rbac token: parse failed')
|
|
end
|
|
|
|
local appid = tokenInfo.appid
|
|
local wolf_token = tokenInfo.wolf_token
|
|
perm_item.appid = appid
|
|
perm_item.wolf_token = wolf_token
|
|
|
|
local consumer_conf = consumer.plugin(plugin_name)
|
|
if not consumer_conf then
|
|
return 401, fail_response("Missing related consumer")
|
|
end
|
|
|
|
local consumers = consumer.consumers_kv(plugin_name, consumer_conf, "appid")
|
|
|
|
core.log.info("------ consumers: ", core.json.delay_encode(consumers))
|
|
local cur_consumer = consumers[appid]
|
|
if not cur_consumer then
|
|
core.log.error("consumer [", appid, "] not found")
|
|
return 401, fail_response("Invalid appid in rbac token")
|
|
end
|
|
core.log.info("consumer: ", core.json.delay_encode(cur_consumer))
|
|
local server = cur_consumer.auth_conf.server
|
|
|
|
local res = check_url_permission(server, appid, action, url,
|
|
client_ip, wolf_token)
|
|
core.log.info(" check_url_permission(", core.json.delay_encode(perm_item),
|
|
") res: ",core.json.delay_encode(res))
|
|
|
|
local username = nil
|
|
local nickname = nil
|
|
if type(res.userInfo) == 'table' then
|
|
local userInfo = res.userInfo
|
|
ctx.userInfo = userInfo
|
|
local userId = userInfo.id
|
|
username = userInfo.username
|
|
nickname = userInfo.nickname or userInfo.username
|
|
local prefix = cur_consumer.auth_conf.header_prefix or ''
|
|
core.response.set_header(prefix .. "UserId", userId)
|
|
core.response.set_header(prefix .. "Username", username)
|
|
core.response.set_header(prefix .. "Nickname", ngx.escape_uri(nickname))
|
|
core.request.set_header(ctx, prefix .. "UserId", userId)
|
|
core.request.set_header(ctx, prefix .. "Username", username)
|
|
core.request.set_header(ctx, prefix .. "Nickname", ngx.escape_uri(nickname))
|
|
end
|
|
|
|
if res.status ~= 200 then
|
|
-- no permission.
|
|
core.log.error(" check_url_permission(",
|
|
core.json.delay_encode(perm_item),
|
|
") failed, res: ",core.json.delay_encode(res))
|
|
return res.status, fail_response(res.err, { username = username, nickname = nickname })
|
|
end
|
|
consumer.attach_consumer(ctx, cur_consumer, consumer_conf)
|
|
core.log.info("wolf-rbac check permission passed")
|
|
end
|
|
|
|
local function get_args()
|
|
local ctx = ngx.ctx.api_ctx
|
|
local args, err
|
|
req_read_body()
|
|
if string.find(ctx.var.http_content_type or "","application/json",
|
|
1, true) then
|
|
local req_body = req_get_body_data()
|
|
args, err = json.decode(req_body)
|
|
if not args then
|
|
core.log.error("json.decode(", req_body, ") failed! ", err)
|
|
end
|
|
else
|
|
args = core.request.get_post_args(ctx)
|
|
end
|
|
|
|
return args
|
|
end
|
|
|
|
local function get_consumer(appid)
|
|
local consumer_conf = consumer.plugin(plugin_name)
|
|
if not consumer_conf then
|
|
core.response.exit(500)
|
|
end
|
|
|
|
local consumers = consumer.consumers_kv(plugin_name, consumer_conf, "appid")
|
|
|
|
core.log.info("------ consumers: ", core.json.delay_encode(consumers))
|
|
local consumer = consumers[appid]
|
|
if not consumer then
|
|
core.log.info("request appid [", appid, "] not found")
|
|
core.response.exit(400,
|
|
fail_response("appid not found")
|
|
)
|
|
end
|
|
return consumer
|
|
end
|
|
|
|
local function request_to_wolf_server(method, uri, headers, body)
|
|
headers["Content-Type"] = "application/json; charset=utf-8"
|
|
local timeout = 1000 * 5
|
|
local request_debug = core.json.delay_encode(
|
|
{
|
|
method = method, uri = uri, body = body,
|
|
headers = headers,timeout = timeout
|
|
}
|
|
)
|
|
|
|
core.log.info("request [", request_debug, "] ....")
|
|
local res, err = http_req(method, uri, core.json.encode(body), headers, timeout)
|
|
if not res then
|
|
core.log.error("request [", request_debug, "] failed! err: ", err)
|
|
return core.response.exit(500,
|
|
fail_response("request to wolf-server failed!")
|
|
)
|
|
end
|
|
core.log.info("request [", request_debug, "] status: ", res.status,
|
|
", body: ", res.body)
|
|
|
|
if res.status ~= 200 then
|
|
core.log.error("request [", request_debug, "] failed! status: ",
|
|
res.status)
|
|
return core.response.exit(500,
|
|
fail_response("request to wolf-server failed!")
|
|
)
|
|
end
|
|
local body, err = json.decode(res.body)
|
|
if not body then
|
|
core.log.error("request [", request_debug, "] failed! err:", err)
|
|
return core.response.exit(500, fail_response("request to wolf-server failed!"))
|
|
end
|
|
if not body.ok then
|
|
core.log.error("request [", request_debug, "] failed! response body:",
|
|
core.json.delay_encode(body))
|
|
return core.response.exit(200, fail_response("request to wolf-server failed!"))
|
|
end
|
|
|
|
core.log.info("request [", request_debug, "] success! response body:",
|
|
core.json.delay_encode(body))
|
|
return body
|
|
end
|
|
|
|
local function wolf_rbac_login()
|
|
local args = get_args()
|
|
if not args then
|
|
return core.response.exit(400, fail_response("invalid request"))
|
|
end
|
|
if not args.appid then
|
|
return core.response.exit(400, fail_response("appid is missing"))
|
|
end
|
|
|
|
local appid = args.appid
|
|
local consumer = get_consumer(appid)
|
|
core.log.info("consumer: ", core.json.delay_encode(consumer))
|
|
|
|
local uri = consumer.auth_conf.server .. '/wolf/rbac/login.rest'
|
|
local headers = new_headers()
|
|
local body = request_to_wolf_server('POST', uri, headers, args)
|
|
|
|
local userInfo = body.data.userInfo
|
|
local wolf_token = body.data.token
|
|
|
|
local rbac_token = create_rbac_token(appid, wolf_token)
|
|
core.response.exit(200, success_response(nil, {rbac_token = rbac_token, user_info = userInfo}))
|
|
end
|
|
|
|
local function get_wolf_token(ctx)
|
|
core.log.info("hit wolf-rbac change_password api")
|
|
local rbac_token = fetch_rbac_token(ctx)
|
|
if rbac_token == nil then
|
|
local url = ctx.var.uri
|
|
local action = ctx.var.request_method
|
|
local client_ip = core.request.get_ip(ctx)
|
|
local perm_item = {action = action, url = url, clientIP = client_ip}
|
|
core.log.info("no permission to access ",
|
|
core.json.delay_encode(perm_item), ", need login!")
|
|
return core.response.exit(401, fail_response("Missing rbac token in request"))
|
|
end
|
|
|
|
local tokenInfo, err = parse_rbac_token(rbac_token)
|
|
core.log.info("token info: ", core.json.delay_encode(tokenInfo),
|
|
", err: ", err)
|
|
if err then
|
|
return core.response.exit(401, fail_response('invalid rbac token: parse failed'))
|
|
end
|
|
return tokenInfo
|
|
end
|
|
|
|
local function wolf_rbac_change_pwd()
|
|
local args = get_args()
|
|
|
|
local ctx = ngx.ctx.api_ctx
|
|
local tokenInfo = get_wolf_token(ctx)
|
|
local appid = tokenInfo.appid
|
|
local wolf_token = tokenInfo.wolf_token
|
|
local consumer = get_consumer(appid)
|
|
core.log.info("consumer: ", core.json.delay_encode(consumer))
|
|
|
|
local uri = consumer.auth_conf.server .. '/wolf/rbac/change_pwd'
|
|
local headers = new_headers()
|
|
headers['x-rbac-token'] = wolf_token
|
|
request_to_wolf_server('POST', uri, headers, args)
|
|
core.response.exit(200, success_response('success to change password', { }))
|
|
end
|
|
|
|
local function wolf_rbac_user_info()
|
|
local ctx = ngx.ctx.api_ctx
|
|
local tokenInfo = get_wolf_token(ctx)
|
|
local appid = tokenInfo.appid
|
|
local wolf_token = tokenInfo.wolf_token
|
|
local consumer = get_consumer(appid)
|
|
core.log.info("consumer: ", core.json.delay_encode(consumer))
|
|
|
|
local uri = consumer.auth_conf.server .. '/wolf/rbac/user_info'
|
|
local headers = new_headers()
|
|
headers['x-rbac-token'] = wolf_token
|
|
local body = request_to_wolf_server('GET', uri, headers, {})
|
|
local userInfo = body.data.userInfo
|
|
core.response.exit(200, success_response(nil, {user_info = userInfo}))
|
|
end
|
|
|
|
function _M.api()
|
|
return {
|
|
{
|
|
methods = {"POST"},
|
|
uri = "/apisix/plugin/wolf-rbac/login",
|
|
handler = wolf_rbac_login,
|
|
},
|
|
{
|
|
methods = {"PUT"},
|
|
uri = "/apisix/plugin/wolf-rbac/change_pwd",
|
|
handler = wolf_rbac_change_pwd,
|
|
},
|
|
{
|
|
methods = {"GET"},
|
|
uri = "/apisix/plugin/wolf-rbac/user_info",
|
|
handler = wolf_rbac_user_info,
|
|
},
|
|
}
|
|
end
|
|
|
|
return _M
|