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