| -- |
| -- 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 ipairs = ipairs |
| local ngx = ngx |
| local tostring = tostring |
| 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_post_args = ngx.req.get_post_args |
| local req_get_body_data = ngx.req.get_body_data |
| |
| local plugin_name = "wolf-rbac" |
| |
| |
| local lrucache = core.lrucache.new({ |
| type = "plugin", |
| }) |
| |
| local schema = { |
| type = "object", |
| properties = { |
| appid = { |
| type = "string", |
| default = "unset" |
| }, |
| server = { |
| type = "string", |
| default = "http://127.0.0.1:10080" |
| }, |
| header_prefix = { |
| type = "string", |
| default = "X-" |
| }, |
| } |
| } |
| |
| local _M = { |
| version = 0.1, |
| priority = 2555, |
| type = 'auth', |
| name = plugin_name, |
| schema = schema, |
| } |
| |
| |
| local create_consume_cache |
| do |
| local consumer_names = {} |
| |
| function create_consume_cache(consumers) |
| core.table.clear(consumer_names) |
| |
| for _, consumer in ipairs(consumers.nodes) do |
| core.log.info("consumer node: ", core.json.delay_encode(consumer)) |
| consumer_names[consumer.auth_conf.appid] = consumer |
| end |
| |
| return consumer_names |
| end |
| |
| end -- do |
| |
| 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 myheaders == nil then myheaders = new_headers() end |
| |
| local httpc = http.new() |
| if timeout then |
| httpc:set_timeout(timeout) |
| end |
| |
| local params = {method = method, headers = myheaders, body = body, |
| ssl_verify = false} |
| local res, err = httpc:request_uri(uri, params) |
| if err 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) |
| 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 ~= 401 then |
| return { |
| status = 500, |
| err = 'request to wolf-server failed, status:' .. res.status |
| } |
| end |
| |
| local body, err = json.decode(res.body) |
| if err 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 = lrucache("consumers_key", consumer_conf.conf_version, |
| create_consume_cache, consumer_conf) |
| |
| core.log.info("------ consumers: ", core.json.delay_encode(consumers)) |
| local consumer = consumers[appid] |
| if not 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(consumer)) |
| local server = 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 = 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, ctx) |
| 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 401, fail_response(res.err, |
| { username = username, nickname = nickname } |
| ) |
| end |
| 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 err then |
| core.log.error("json.decode(", req_body, ") failed! ", err) |
| end |
| else |
| args = req_get_post_args() |
| 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 = lrucache("consumers_key", consumer_conf.conf_version, |
| create_consume_cache, consumer_conf) |
| |
| 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 [" .. tostring(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 err or not res then |
| core.log.error("request [", request_debug, "] failed! err: ", err) |
| return core.response.exit(500, |
| fail_response("request to wolf-server failed! " .. tostring(err)) |
| ) |
| 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! status:" |
| .. tostring(res.status)) |
| ) |
| end |
| local body, err = json.decode(res.body) |
| if err or 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(body.reason)) |
| 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 |