diff --git a/.travis/ASF-Release.cfg b/.travis/ASF-Release.cfg index e967bdcccf3d..703a3611f957 100644 --- a/.travis/ASF-Release.cfg +++ b/.travis/ASF-Release.cfg @@ -87,7 +87,6 @@ grpc_server_example .travis.yml grpcurl t/servroot -grpcurl conf .travis/openwhisk-utilities diff --git a/conf/config.yaml b/conf/config.yaml index 18d1ec3f4113..261d57a68f22 100644 --- a/conf/config.yaml +++ b/conf/config.yaml @@ -95,6 +95,7 @@ plugins: # plugin list - response-rewrite - fault-injection - udp-logger + - wolf-rbac stream_plugins: - mqtt-proxy diff --git a/doc/images/plugin/wolf-rbac-1.png b/doc/images/plugin/wolf-rbac-1.png new file mode 100644 index 000000000000..ce888d88289c Binary files /dev/null and b/doc/images/plugin/wolf-rbac-1.png differ diff --git a/doc/images/plugin/wolf-rbac-2.png b/doc/images/plugin/wolf-rbac-2.png new file mode 100644 index 000000000000..e1223c927268 Binary files /dev/null and b/doc/images/plugin/wolf-rbac-2.png differ diff --git a/doc/plugins/wolf-rbac-cn.md b/doc/plugins/wolf-rbac-cn.md new file mode 100644 index 000000000000..37c9dd7b85ba --- /dev/null +++ b/doc/plugins/wolf-rbac-cn.md @@ -0,0 +1,207 @@ + + +[English](wolf-rbac.md) + +# 目录 + +- [**名字**](#名字) +- [**属性**](#属性) +- [**依赖项**](#依赖项) +- [**如何启用**](#如何启用) +- [**测试插件**](#测试插件) +- [**禁用插件**](#禁用插件) + +## 名字 + +`wolf-rbac` 是一个认证及授权(rbac)插件,它需要与 `consumer` 一起配合才能工作。同时需要添加 `wolf-rbac` 到一个 `service` 或 `route` 中。 +rbac功能由[wolf](https://github.com/iGeeky/wolf)提供, 有关 `wolf` 的更多信息, 请参考[wolf文档](https://github.com/iGeeky/wolf)。 + + +## 属性 + +* `server`: 设置 `wolf-server` 的访问地址, 如果未设置, 默认为: `http://127.0.0.1:10080`. +* `appid`: 设置应用id, 该应用id, 需要是在 `wolf-console` 中已经添加的应用id. + + +## 依赖项 + +### 安装 wolf, 并启动服务 + +[Wolf快速起步](https://github.com/iGeeky/wolf/blob/master/quick-start-with-docker/README-CN.md) + +### 添加应用, 管理员, 普通用户, 权限, 资源 及给用户授权. + +[Wolf管理使用](https://github.com/iGeeky/wolf/blob/master/docs/usage.md) + + +## 如何启用 + +1. 创建一个 consumer 对象,并设置插件 `wolf-rbac` 的值。 + +```shell +curl http://127.0.0.1:9080/apisix/admin/consumers -X PUT -d ' +{ + "username":"wolf_rbac", + "plugins":{ + "wolf-rbac":{ + "server":"http://127.0.0.1:10080", + "appid":"restful" + } + }, + "desc":"wolf-rbac" +}' +``` + +你可以使用浏览器打开 dashboard:`http://127.0.0.1:9080/apisix/dashboard/`,通过 web 界面来完成上面的操作,先增加一个 consumer: +![](../images/plugin/wolf-rbac-1.png) + +然后在 consumer 页面中添加 wolf-rbac 插件: +![](../images/plugin/wolf-rbac-2.png) + +注意: 上面填写的 `appid` 需要在wolf控制台中已经存在的. + +2. 创建 Route 或 Service 对象,并开启 `wolf-rbac` 插件。 + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d ' +{ + "methods": ["GET"], + "uri": "/*", + "plugins": { + "wolf-rbac": {} + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "www.baidu.com:80": 1 + } + } +}' +``` + +## 测试插件 + +#### 首先进行登录获取 `wolf-rbac` token: + +下面的 `appid`, `username`, `password` 必须为wolf系统中真实存在的. + +* 以POST application/json方式登陆. + +```shell +curl http://127.0.0.1:9080/apisix/plugin/wolf-rbac/login -i \ +-H "Content-Type: application/json" \ +-d '{"appid": "restful", "username":"test", "password":"user-password"}' + +HTTP/1.1 200 OK +Date: Wed, 24 Jul 2019 10:33:31 GMT +Content-Type: text/plain +Transfer-Encoding: chunked +Connection: keep-alive +Server: APISIX web server +{"rbac_token":"V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts","user_info":{"nickname":"test","username":"test","id":"749"}} +``` + +* 以POST x-www-form-urlencoded方式登陆 + +```shell +curl http://127.0.0.1:9080/apisix/plugin/wolf-rbac/login -i \ +-H "Content-Type: application/x-www-form-urlencoded" \ +-d 'appid=restful&username=test&password=user-password' +``` + + +#### 使用获取到的 token 进行请求尝试 + +* 缺少 token + +```shell +curl http://127.0.0.1:9080/ -H"Host: www.baidu.com" -i + +HTTP/1.1 401 Unauthorized +... +{"message":"Missing rbac token in request"} +``` + +* token 放到请求头(Authorization)中: + +```shell +curl http://127.0.0.1:9080/ -H"Host: www.baidu.com" \ +-H 'Authorization: V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts' -i + +HTTP/1.1 200 OK + + +``` + +* token 放到请求头(x-rbac-token)中: + +```shell +curl http://127.0.0.1:9080/ -H"Host: www.baidu.com" \ +-H 'x-rbac-token: V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts' -i + + +HTTP/1.1 200 OK + + +``` + +* token 放到请求参数中: + +```shell +curl 'http://127.0.0.1:9080?rbac_token=V1%23restful%23eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts' -H"Host: www.baidu.com" -i + + +HTTP/1.1 200 OK + + +``` + +* token 放到 cookie 中: + +```shell +curl http://127.0.0.1:9080 -H"Host: www.baidu.com" \ +--cookie x-rbac-token=V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts -i + + +HTTP/1.1 200 OK + + +``` + +## 禁用插件 + +当你想去掉 `rbac-wolf` 插件的时候,很简单,在routes中的插件配置中把对应的 `插件` 配置删除即可,无须重启服务,即刻生效: + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d ' +{ + "methods": ["GET"], + "uri": "/*", + "plugins": { + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "www.baidu.com:80": 1 + } + } +}' +``` + diff --git a/doc/plugins/wolf-rbac.md b/doc/plugins/wolf-rbac.md new file mode 100644 index 000000000000..639070b59df1 --- /dev/null +++ b/doc/plugins/wolf-rbac.md @@ -0,0 +1,209 @@ + + +[中文](wolf-rbac-cn.md) + +# Summary + +- [**Name**](#name) +- [**Attributes**](#attributes) +- [**Dependencies**](#dependencies) +- [**How To Enable**](#how-to-enable) +- [**Test Plugin**](#test-plugin) +- [**Disable Plugin**](#disable-plugin) + +## Name + +`wolf-rbac` is an authentication and authorization (rbac) plugin. It needs to work with `consumer`. Also need to add `wolf-rbac` to a` service` or `route`. +The rbac feature is provided by [wolf] (https://github.com/iGeeky/wolf). For more information about `wolf`, please refer to [wolf documentation] (https://github.com/iGeeky/wolf). + + +## Attributes + +* `server`: Set the service address of` wolf-server`. If not set, the default is: `http://127.0.0.1:10080`. +* `appid`: Set the app id. The app id must be added in wolf-console. + + +## Dependencies + +### Install wolf and start the service + +[Wolf quick start](https://github.com/iGeeky/wolf/blob/master/quick-start-with-docker/README.md) + +### Add `application`,` admin`, `normal user`,` permission`, `resource` and user authorize + +[Wolf-console usage](https://github.com/iGeeky/wolf/blob/master/docs/usage.md) + + +## How To Enable + +1. set a consumer and config the value of the `wolf-rbac`。 + +```shell +curl http://127.0.0.1:9080/apisix/admin/consumers -X PUT -d ' +{ + "username":"wolf_rbac", + "plugins":{ + "wolf-rbac":{ + "server":"http://127.0.0.1:10080", + "appid":"restful" + } + }, + "desc":"wolf-rbac" +}' +``` + +You can visit the dashboard: `http://127.0.0.1:9080/apisix/dashboard/`, to complete the above operations through the web interface, first add a consumer: +![](../images/plugin/wolf-rbac-1.png) + +Then add the wolf-rbac plugin to the consumer page: +![](../images/plugin/wolf-rbac-2.png) + +Notes: The `appid` filled in above needs to already exist in the wolf system. + +1. Add a `Route` or `Service` and enable the wolf-rbac plugin. + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d ' +{ + "methods": ["GET"], + "uri": "/*", + "plugins": { + "wolf-rbac": {} + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "www.baidu.com:80": 1 + } + } +}' +``` + +## Test Plugin + +#### Login and get `wolf-rbac` token: + +The following `appid`,` username`, and `password` must be real ones in the wolf system. + +* Login as `POST application/json` + +```shell +curl http://127.0.0.1:9080/apisix/plugin/wolf-rbac/login -i \ +-H "Content-Type: application/json" \ +-d '{"appid": "restful", "username":"test", "password":"user-password"}' + +HTTP/1.1 200 OK +Date: Wed, 24 Jul 2019 10:33:31 GMT +Content-Type: text/plain +Transfer-Encoding: chunked +Connection: keep-alive +Server: APISIX web server +{"rbac_token":"V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts","user_info":{"nickname":"test","username":"test","id":"749"}} +``` + +* Login as `POST x-www-form-urlencoded` + +```shell +curl http://127.0.0.1:9080/apisix/plugin/wolf-rbac/login -i \ +-H "Content-Type: application/x-www-form-urlencoded" \ +-d 'appid=restful&username=test&password=user-password' +``` + + +#### try request with token + +* without token + +```shell +curl http://127.0.0.1:9080/ -H"Host: www.baidu.com" -i + +HTTP/1.1 401 Unauthorized +... +{"message":"Missing rbac token in request"} +``` + +* request header(Authorization) with token: + +```shell +curl http://127.0.0.1:9080/ -H"Host: www.baidu.com" \ +-H 'Authorization: V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts' -i + +HTTP/1.1 200 OK + + +``` + +* request header(x-rbac-token) with token: + +```shell +curl http://127.0.0.1:9080/ -H"Host: www.baidu.com" \ +-H 'x-rbac-token: V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts' -i + + +HTTP/1.1 200 OK + + +``` + +* request params with token: + +```shell +curl 'http://127.0.0.1:9080?rbac_token=V1%23restful%23eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts' -H"Host: www.baidu.com" -i + + +HTTP/1.1 200 OK + + +``` + +* request cookie with token: + +```shell +curl http://127.0.0.1:9080 -H"Host: www.baidu.com" \ +--cookie x-rbac-token=V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts -i + + +HTTP/1.1 200 OK + + +``` + +## Disable Plugin + +When you want to disable the `wolf-rbac` plugin, it is very simple, + you can delete the corresponding json configuration in the plugin configuration, + no need to restart the service, it will take effect immediately: + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d ' +{ + "methods": ["GET"], + "uri": "/*", + "plugins": { + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "www.baidu.com:80": 1 + } + } +}' +``` + diff --git a/lua/apisix/plugins/wolf-rbac.lua b/lua/apisix/plugins/wolf-rbac.lua new file mode 100644 index 000000000000..b38501ea1016 --- /dev/null +++ b/lua/apisix/plugins/wolf-rbac.lua @@ -0,0 +1,386 @@ +-- +-- 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 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 plugin_name = "wolf-rbac" + + +local schema = { + type = "object", + properties = { + appid = { + type = "string", + default = "unset" + }, + server = { + type = "string", + default = "http://127.0.0.1:10080" + }, + } +} + +local _M = { + version = 0.1, + priority = 2555, + type = 'auth', + name = plugin_name, + schema = schema, +} + + +local create_consume_cache +do + local consumer_ids = {} + + function create_consume_cache(consumers) + core.table.clear(consumer_ids) + + for _, consumer in ipairs(consumers.nodes) do + core.log.info("consumer node: ", core.json.delay_encode(consumer)) + consumer_ids[consumer.auth_conf.appid] = consumer + end + + return consumer_ids + end + +end -- do + +local token_version = 'V1' +local function create_rbac_token(appid, wolf_token) + return token_version .. "#" .. appid .. "#" .. wolf_token +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 + +local function http_post(uri, body, myheaders, timeout) + return http_req("POST", uri, body, 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, clientIP, 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 = clientIP} + 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 + ngx.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:" .. tostring(err)} + end + + if res.status ~= 200 and res.status ~= 401 then + return {status = 500, err = 'request to wolf-server failed, status:' .. tostring(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 clientIP = core.request.get_ip(ctx) + local permItem = {action = action, url = url, clientIP = clientIP} + 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(permItem), ", need login!") + return 401, {message = "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, {message = 'invalid rbac token: parse failed'} + end + + local appid = tokenInfo.appid + local wolf_token = tokenInfo.wolf_token + permItem.appid = appid + permItem.wolf_token = wolf_token + + local consumer_conf = consumer.plugin(plugin_name) + if not consumer_conf then + return 401, {message = "Missing related consumer"} + end + + local consumers = core.lrucache.plugin(plugin_name, "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, {message = "Invalid appid in rbac token"} + end + core.log.info("consumer: ", core.json.delay_encode(consumer)) + local server = consumer.auth_conf.server + + local url = ctx.var.uri + local action = ctx.var.request_method + local clientIP = core.request.get_ip(ctx) + local permItem = {appid = appid, action = action, url = url, clientIP = clientIP, wolf_token = wolf_token} + + local res = check_url_permission(server, appid, action, url, clientIP, wolf_token) + core.log.info(" check_url_permission(", core.json.delay_encode(permItem), ") res: ",core.json.delay_encode(res)) + + local username = nil + local nickname = nil + if type(res.userInfo) == 'table' then + local userInfo = res.userInfo + core.response.set_header("X-UserId", userInfo.id) + core.response.set_header("X-Username", userInfo.username) + core.response.set_header("X-Nickname", ngx.escape_uri(userInfo.nickname) or userInfo.username) + ctx.userInfo = userInfo + username = userInfo.username + nickname = userInfo.nickname + end + + if res.status ~= 200 then + -- no permission. + core.log.error(" check_url_permission(", core.json.delay_encode(permItem), + ") failed, res: ",core.json.delay_encode(res)) + return 401, {message = 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 + ngx.req.read_body() + if string.find(ctx.var.http_content_type or "","application/json", 1, true) then + args, err = json.decode(ngx.req.get_body_data()) + if err then + core.log.error("json.decode(", ngx.req.get_body_data(), ") failed! ", err) + end + else + args = ngx.req.get_post_args() + end + return args +end + +local function login() + local args = get_args() + if not args then + return core.response.exit(400, {message = "invalid request"}) + end + if not args.appid then + return core.response.exit(400, {message = "appid is missing"}) + end + + local appid = args.appid + + local consumer_conf = consumer.plugin(plugin_name) + if not consumer_conf then + return core.response.exit(500) + end + + local consumers = core.lrucache.plugin(plugin_name, "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") + return core.response.exit(400, {message = "appid [" .. tostring(appid) .. "] not found"}) + end + + core.log.info("consumer: ", core.json.delay_encode(consumer)) + + local uri = consumer.auth_conf.server .. '/wolf/rbac/login.rest' + local headers = new_headers() + headers["Content-Type"] = "application/json; charset=utf-8" + local timeout = 1000 * 5 + local request_debug = core.json.delay_encode( + {method = 'POST', uri = uri, body = args, headers = headers,timeout = timeout}) + core.log.info("login request [", request_debug, "] ....") + local res, err = http_post(uri, core.json.encode(args), headers, timeout) + if err or not res then + core.log.error("login request [", request_debug, "] failed! err: ", err) + return core.response.exit(500, {message = "request to wolf-server failed! " .. tostring(err)}) + end + core.log.info("login request [", request_debug, "] status: ", res.status, ", body: ", res.body) + + if res.status ~= 200 then + core.log.error("login request [", request_debug, "] failed! status: ", res.status) + return core.response.exit(500, {message = "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("login request [", request_debug, "] failed! err:", err) + return core.response.exit(500, {message = "request to wolf-server failed!"}) + end + if not body.ok then + core.log.error("user login [", request_debug, "] failed! response body:", core.json.delay_encode(body)) + return core.response.exit(200, {message = body.reason}) + end + core.log.info("user login [", request_debug, "] success! response body:", core.json.delay_encode(body)) + + local userInfo = body.data.userInfo + local wolf_token = body.data.token + + local rbac_token = create_rbac_token(appid, wolf_token) + core.response.exit(200, {rbac_token = rbac_token, user_info = userInfo}) +end + +function _M.api() + return { + { + methods = {"POST"}, + uri = "/apisix/plugin/wolf-rbac/login", + handler = login, + } + } +end + +return _M diff --git a/t/admin/plugins.t b/t/admin/plugins.t index b36a49140562..031eaff9f961 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -30,6 +30,6 @@ __DATA__ --- request GET /apisix/admin/plugins/list --- response_body_like eval -qr/\["limit-req","limit-count","limit-conn","key-auth","basic-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect","response-rewrite","fault-injection",["udp-logger"]\]/ +qr/\["limit-req","limit-count","limit-conn","key-auth","basic-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect","response-rewrite","fault-injection","udp-logger","wolf-rbac"\]/ --- no_error_log [error] diff --git a/t/debug/debug-mode.t b/t/debug/debug-mode.t index 23b6e90b1f11..b069a23d0eca 100644 --- a/t/debug/debug-mode.t +++ b/t/debug/debug-mode.t @@ -59,6 +59,7 @@ loaded plugin and sort by priority: 11000 name: fault-injection loaded plugin and sort by priority: 10000 name: serverless-pre-function loaded plugin and sort by priority: 3000 name: ip-restriction loaded plugin and sort by priority: 2599 name: openid-connect +loaded plugin and sort by priority: 2555 name: wolf-rbac loaded plugin and sort by priority: 2520 name: basic-auth loaded plugin and sort by priority: 2510 name: jwt-auth loaded plugin and sort by priority: 2500 name: key-auth diff --git a/t/lib/server.lua b/t/lib/server.lua index 60f57d97f3dc..a843158ae2f5 100644 --- a/t/lib/server.lua +++ b/t/lib/server.lua @@ -15,6 +15,7 @@ -- limitations under the License. -- local json_decode = require("cjson").decode +local json_encode = require("cjson").encode local _M = {} @@ -31,6 +32,9 @@ end function _M.server_port() ngx.print(ngx.var.server_port) end +_M.server_port_route2 = _M.server_port +_M.server_port_hello = _M.server_port +_M.server_port_aa = _M.server_port function _M.limit_conn() @@ -69,6 +73,8 @@ function _M.uri() ngx.say(k, ": ", v) end end +_M.uri_plugin_proxy_rewrite = _M.uri +_M.uri_plugin_proxy_rewrite_args = _M.uri function _M.old_uri() -- ngx.sleep(1) @@ -112,6 +118,50 @@ function _M.mock_zipkin() end end +function _M.wolf_rbac_login_rest() + ngx.req.read_body() + local data = ngx.req.get_body_data() + local args = json_decode(data) + if not args.username then + ngx.say(json_encode({ok=false, reason="ERR_USERNAME_MISSING"})) + ngx.exit(0) + end + if not args.password then + ngx.say(json_encode({ok=false, reason="ERR_PASSWORD_MISSING"})) + ngx.exit(0) + end + if args.username ~= "admin" then + ngx.say(json_encode({ok=false, reason="ERR_USER_NOT_FOUND"})) + ngx.exit(0) + end + if args.password ~= "123456" then + ngx.say(json_encode({ok=false, reason="ERR_PASSWORD_ERROR"})) + ngx.exit(0) + end + + ngx.say(json_encode({ok=true, data={token="wolf-rbac-token", + userInfo={nickname="administrator",username="admin", id="100"}}})) +end + +function _M.wolf_rbac_access_check() + local headers = ngx.req.get_headers() + local token = headers['x-rbac-token'] + if token ~= 'wolf-rbac-token' then + ngx.say(json_encode({ok=false, reason="ERR_TOKEN_INVALID"})) + ngx.exit(0) + end + + local args = ngx.req.get_uri_args() + local resName = args.resName + if resName == '/hello' then + ngx.say(json_encode({ok=true, data={ userInfo={nickname="administrator",username="admin", id="100"} }})) + else + ngx.status = 401 + ngx.say(json_encode({ok=false, reason="no permission to access"})) + end +end + + function _M.websocket_handshake() local websocket = require "resty.websocket.server" local wb, err = websocket:new() @@ -120,15 +170,11 @@ function _M.websocket_handshake() return ngx.exit(400) end end - +_M.websocket_handshake_route = _M.websocket_handshake function _M.go() local action = string.sub(ngx.var.uri, 2) - local find = string.find(action, "/", 1, true) - if find then - action = string.sub(action, 1, find - 1) - end - + action = string.gsub(action, "[/\\.]", "_") if not action or not _M[action] then return ngx.exit(404) end diff --git a/t/plugin/wolf-rbac.t b/t/plugin/wolf-rbac.t new file mode 100644 index 000000000000..e90e30775880 --- /dev/null +++ b/t/plugin/wolf-rbac.t @@ -0,0 +1,334 @@ +# +# 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. +# +use t::APISIX 'no_plan'; + + +repeat_each(1); +no_long_string(); +no_root_location(); +no_shuffle(); +run_tests; + +__DATA__ + +=== TEST 1: sanity +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.wolf-rbac") + local conf = { + + } + + local ok, err = plugin.check_schema(conf) + if not ok then + ngx.say(err) + end + + ngx.say(require("cjson").encode(conf)) + } + } +--- request +GET /t +--- response_body_like eval +qr/{"appid":"unset","server":"http:\\\/\\\/127\.0\.0\.1:10080"}/ +--- no_error_log +[error] + +=== TEST 2: wrong type of string +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.wolf-rbac") + local ok, err = plugin.check_schema({appid = 123}) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- request +GET /t +--- response_body +property "appid" validation failed: wrong type: expected string, got number +done +--- no_error_log +[error] + + +=== TEST 3: add consumer with username and plugins +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "wolf_rbac_unit_test", + "plugins": { + "wolf-rbac": { + "appid": "wolf-rbac-app", + "server": "http://127.0.0.1:1982" + } + } + }]], + [[{ + "node": { + "value": { + "username": "wolf_rbac_unit_test", + "plugins": { + "wolf-rbac": { + "appid": "wolf-rbac-app", + "server": "http://127.0.0.1:1982" + } + } + } + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + +=== TEST 4: enable wolf rbac plugin using admin api +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "wolf-rbac": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello*" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + +=== TEST 5: login failed, appid is missing +--- request +POST /apisix/plugin/wolf-rbac/login +username=admin&password=123456 +--- more_headers +Content-Type: application/x-www-form-urlencoded +--- error_code: 400 +--- response_body_like eval +qr/appid is missing/ +--- no_error_log +[error] + + +=== TEST 6: login failed, appid not found +--- request +POST /apisix/plugin/wolf-rbac/login +appid=not-found&username=admin&password=123456 +--- more_headers +Content-Type: application/x-www-form-urlencoded +--- error_code: 400 +--- response_body_like eval +qr/appid \[not-found\] not found/ +--- no_error_log +[error] + + +=== TEST 7: login failed, username missing +--- request +POST /apisix/plugin/wolf-rbac/login +appid=wolf-rbac-app&password=123456 +--- more_headers +Content-Type: application/x-www-form-urlencoded +--- error_code: 200 +--- response_body_like eval +qr/ERR_USERNAME_MISSING/ + + +=== TEST 8: login failed, password missing +--- request +POST /apisix/plugin/wolf-rbac/login +appid=wolf-rbac-app&username=admin +--- more_headers +Content-Type: application/x-www-form-urlencoded +--- error_code: 200 +--- response_body_like eval +qr/ERR_PASSWORD_MISSING/ + + +=== TEST 9: login failed, username not found +--- request +POST /apisix/plugin/wolf-rbac/login +appid=wolf-rbac-app&username=not-found&password=123456 +--- more_headers +Content-Type: application/x-www-form-urlencoded +--- error_code: 200 +--- response_body_like eval +qr/ERR_USER_NOT_FOUND/ + + +=== TEST 10: login failed, wrong password +--- request +POST /apisix/plugin/wolf-rbac/login +appid=wolf-rbac-app&username=admin&password=wrong-password +--- more_headers +Content-Type: application/x-www-form-urlencoded +--- error_code: 200 +--- response_body_like eval +qr/ERR_PASSWORD_ERROR/ + + +=== TEST 11: login successfully +--- request +POST /apisix/plugin/wolf-rbac/login +{"appid": "wolf-rbac-app", "username": "admin","password": "123456"} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- response_body_like eval +qr/{"rbac_token":"V1#wolf-rbac-app#wolf-rbac-token","user_info":{"nickname":"administrator","username":"admin","id":"100"}}/ +--- no_error_log +[error] + + + +=== TEST 21: verify, missing token +--- request +GET /hello +--- error_code: 401 +--- response_body +{"message":"Missing rbac token in request"} +--- no_error_log +[error] + + +=== TEST 22: verify: invalid rbac token +--- request +GET /hello +--- error_code: 401 +--- more_headers +x-rbac-token: invalid-rbac-token +--- response_body +{"message":"invalid rbac token: parse failed"} +--- no_error_log +[error] + + +=== TEST 23: verify: invalid appid in rbac token +--- request +GET /hello +--- error_code: 401 +--- more_headers +x-rbac-token: V1#invalid-appid#rbac-token +--- response_body +{"message":"Invalid appid in rbac token"} + + +=== TEST 24: verify: failed +--- request +GET /hello1 +--- error_code: 401 +--- more_headers +x-rbac-token: V1#wolf-rbac-app#wolf-rbac-token +--- response_body +{"message":"no permission to access"} + + +=== TEST 25: verify (in argument) +--- request +GET /hello?rbac_token=V1%23wolf-rbac-app%23wolf-rbac-token +--- response_headers +X-UserId: 100 +X-Username: admin +X-Nickname: administrator +--- response_body +hello world +--- no_error_log +[error] + + +=== TEST 26: verify (in header Authorization) +--- request +GET /hello +--- more_headers +Authorization: V1#wolf-rbac-app#wolf-rbac-token +--- response_headers +X-UserId: 100 +X-Username: admin +X-Nickname: administrator +--- response_body +hello world +--- no_error_log +[error] + + +=== TEST 27: verify (in header x-rbac-token) +--- request +GET /hello +--- more_headers +x-rbac-token: V1#wolf-rbac-app#wolf-rbac-token +--- response_headers +X-UserId: 100 +X-Username: admin +X-Nickname: administrator +--- response_body +hello world +--- no_error_log +[error] + + +=== TEST 28: verify (in cookie) +--- request +GET /hello +--- more_headers +Cookie: x-rbac-token=V1#wolf-rbac-app#wolf-rbac-token +--- response_headers +X-UserId: 100 +X-Username: admin +X-Nickname: administrator +--- response_body +hello world +--- no_error_log +[error]