You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by we...@apache.org on 2020/01/13 09:43:15 UTC

[incubator-apisix] branch master updated: feature: add basic-auth plugin (#1029)

This is an automated email from the ASF dual-hosted git repository.

wenming pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new 262b990  feature: add basic-auth plugin (#1029)
262b990 is described below

commit 262b9904fb1d90592bf90123544c615842450836
Author: KowloonZh <ko...@gmail.com>
AuthorDate: Mon Jan 13 17:43:04 2020 +0800

    feature: add basic-auth plugin (#1029)
---
 conf/config.yaml                   |   1 +
 doc/images/plugin/basic-auth-1.png | Bin 0 -> 22997 bytes
 doc/images/plugin/basic-auth-2.png | Bin 0 -> 43549 bytes
 doc/plugins/basic-auth-cn.md       | 143 ++++++++++++++++++++++++++
 doc/plugins/basic-auth.md          | 147 +++++++++++++++++++++++++++
 lua/apisix/plugins/basic-auth.lua  | 151 ++++++++++++++++++++++++++++
 t/admin/plugins.t                  |   2 +-
 t/debug/debug-mode.t               |   1 +
 t/plugin/basic-auth.t              | 199 +++++++++++++++++++++++++++++++++++++
 9 files changed, 643 insertions(+), 1 deletion(-)

diff --git a/conf/config.yaml b/conf/config.yaml
index bac8813..844070e 100644
--- a/conf/config.yaml
+++ b/conf/config.yaml
@@ -80,6 +80,7 @@ plugins:                          # plugin list
   - limit-count
   - limit-conn
   - key-auth
+  - basic-auth
   - prometheus
   - node-status
   - jwt-auth
diff --git a/doc/images/plugin/basic-auth-1.png b/doc/images/plugin/basic-auth-1.png
new file mode 100644
index 0000000..0ff8c76
Binary files /dev/null and b/doc/images/plugin/basic-auth-1.png differ
diff --git a/doc/images/plugin/basic-auth-2.png b/doc/images/plugin/basic-auth-2.png
new file mode 100644
index 0000000..906f6f4
Binary files /dev/null and b/doc/images/plugin/basic-auth-2.png differ
diff --git a/doc/plugins/basic-auth-cn.md b/doc/plugins/basic-auth-cn.md
new file mode 100644
index 0000000..4ac16c8
--- /dev/null
+++ b/doc/plugins/basic-auth-cn.md
@@ -0,0 +1,143 @@
+<!--
+#
+# 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.
+#
+-->
+
+[English](basic-auth.md)
+
+# 目录
+- [**名字**](#名字)
+- [**属性**](#属性)
+- [**如何启用**](#如何启用)
+- [**测试插件**](#测试插件)
+- [**禁用插件**](#禁用插件)
+
+
+## 名字
+
+`basic-auth` 是一个认证插件,它需要与 `consumer` 一起配合才能工作。
+
+添加 Basic Authentication 到一个 `service` 或 `route`。 然后 `consumer` 将其用户名和密码添加到请求头中以验证其请求。
+
+有关 Basic Authentication 的更多信息,可参考 [维基百科](https://en.wikipedia.org/wiki/Basic_access_authentication) 查看更多信息。
+
+## 属性
+
+* `username`: 不同的 `consumer` 对象应有不同的值,它应当是唯一的。不同 consumer 使用了相同的 `username` ,将会出现请求匹配异常。
+* `password`: 用户的密码
+
+## 如何启用
+
+1. 创建一个 consumer 对象,并设置插件 `basic-auth` 的值。
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/consumers -X PUT -d '
+{
+    "username": "foo",
+    "plugins": {
+        "basic-auth": {
+            "username": "foo",
+            "password": "bar"
+        }
+    }
+}'
+```
+你可以使用浏览器打开 dashboard:`http://127.0.0.1:9080/apisix/dashboard/`,通过 web 界面来完成上面的操作,先增加一个 consumer:
+![](../images/plugin/basic-auth-1.png)
+
+然后在 consumer 页面中添加 basic-auth 插件:
+![](../images/plugin/basic-auth-2.png)
+
+2. 创建 Route 或 Service 对象,并开启 `basic-auth` 插件。
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d '
+{
+    "methods": ["GET"],
+    "uri": "/hello",
+    "plugins": {
+        "basic-auth": {}
+    },
+    "upstream": {
+        "type": "roundrobin",
+        "nodes": {
+            "127.0.0.1:8080": 1
+        }
+    }
+}'
+```
+
+## Test Plugin
+
+
+* 缺少 Authorization header
+
+```shell
+$ curl http://127.0.0.2:9080/hello -i
+HTTP/1.1 401 Unauthorized
+...
+{"message":"Missing authorization in request"}
+```
+
+* 用户名不存在:
+
+```shell
+$ curl -i -ubar:bar http://127.0.0.1:9080/hello
+HTTP/1.1 401 Unauthorized
+...
+{"message":"Invalid user key in authorization"}
+```
+
+* 密码错误:
+
+```shell
+$ curl -i -ufoo:foo http://127.0.0.1:9080/hello
+HTTP/1.1 401 Unauthorized
+...
+{"message":"Password is error"}
+...
+```
+
+* 成功请求:
+
+```shell
+$ curl -i -ufoo:bar http://127.0.0.1:9080/hello
+HTTP/1.1 200 OK
+...
+hello, foo!
+...
+```
+
+
+## 禁用插件
+
+当你想去掉 `basic-auth` 插件的时候,很简单,在插件的配置中把对应的 `json` 配置删除即可,无须重启服务,即刻生效:
+
+```shell
+$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d '
+{
+    "methods": ["GET"],
+    "uri": "/hello",
+    "plugins": {},
+    "upstream": {
+        "type": "roundrobin",
+        "nodes": {
+            "127.0.0.1:8080": 1
+        }
+    }
+}'
+```
diff --git a/doc/plugins/basic-auth.md b/doc/plugins/basic-auth.md
new file mode 100644
index 0000000..26a9df1
--- /dev/null
+++ b/doc/plugins/basic-auth.md
@@ -0,0 +1,147 @@
+<!--
+#
+# 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.
+#
+-->
+
+[Chinese](basic-auth-cn.md)
+
+# Summary
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+- [**Test Plugin**](#test-plugin)
+- [**Disable Plugin**](#disable-plugin)
+
+
+## Name
+
+`basic-auth` is an authentication plugin that need to work with `consumer`. Add Basic Authentication to a `service` or `route`.
+
+The `consumer` then adds its key to the request header to verify its request.
+
+For more information on Basic authentication, refer to [Wiki](https://en.wikipedia.org/wiki/Basic_access_authenticatio) for more information.
+
+## Attributes
+
+|Name          |Requirement  |Description|
+|---------     |--------|-----------|
+| username |required|different `consumer` have different value, it's unique. different `consumer` use the same `username`, and there will be a request matching exception.|
+| password |required|the user's password|
+
+## How To Enable
+
+1. set a consumer and config the value of the `basic-auth` option
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/consumers -X PUT -d '
+{
+    "username": "foo",
+    "plugins": {
+        "basic-auth": {
+            "username": "foo",
+            "password": "bar"
+        }
+    }
+}'
+```
+
+you can visit Dashboard `http://127.0.0.1:9080/apisix/dashboard/` and add a Consumer through the web console:
+
+![](../images/plugin/basic-auth-1.png)
+
+
+then add basic-auth plugin in the Consumer page:
+
+![](../images/plugin/basic-auth-2.png)
+
+2. add a Route or add a Service , and enable the `basic-auth` plugin
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d '
+{
+    "methods": ["GET"],
+    "uri": "/hello",
+    "plugins": {
+        "basic-auth": {}
+    },
+    "upstream": {
+        "type": "roundrobin",
+        "nodes": {
+            "127.0.0.1:1980": 1
+        }
+    }
+}'
+```
+
+## Test Plugin
+
+* missing Authorization header
+
+```shell
+$ curl http://127.0.0.2:9080/hello -i
+HTTP/1.1 401 Unauthorized
+...
+{"message":"Missing authorization in request"}
+```
+
+* user is not exists:
+
+```shell
+$ curl -i -ubar:bar http://127.0.0.1:9080/hello
+HTTP/1.1 401 Unauthorized
+...
+{"message":"Invalid user key in authorization"}
+```
+
+* password is invalid:
+
+```shell
+$ curl -i -ufoo:foo http://127.0.0.1:9080/hello
+HTTP/1.1 401 Unauthorized
+...
+{"message":"Password is error"}
+```
+
+* success:
+
+```shell
+$ curl -i -ufoo:bar http://127.0.0.1:9080/hello
+HTTP/1.1 200 OK
+...
+hello, world
+```
+
+## Disable Plugin
+
+When you want to disable the `basic-auth` 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:2379/apisix/admin/routes/1 -X PUT -d value='
+{
+    "methods": ["GET"],
+    "uri": "/hello",
+    "plugins": {},
+    "upstream": {
+        "type": "roundrobin",
+        "nodes": {
+            "127.0.0.1:1980": 1
+        }
+    }
+}'
+```
diff --git a/lua/apisix/plugins/basic-auth.lua b/lua/apisix/plugins/basic-auth.lua
new file mode 100644
index 0000000..de9ccbf
--- /dev/null
+++ b/lua/apisix/plugins/basic-auth.lua
@@ -0,0 +1,151 @@
+--
+-- 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 ngx = ngx
+local ngx_re = require("ngx.re")
+local ipairs = ipairs
+local consumer = require("apisix.consumer")
+
+local lrucache = core.lrucache.new({
+    ttl = 300, count = 512
+})
+
+local schema = {
+    type = "object",
+    properties = {
+        username = { type = "string" },
+        password = { type = "string" },
+    },
+}
+
+local plugin_name = "basic-auth"
+
+local _M = {
+    version = 0.1,
+    priority = 2520,
+    type = 'auth',
+    name = plugin_name,
+    schema = schema,
+}
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+local function extract_auth_header(authorization)
+
+    local function do_extract(auth)
+        local obj = { username = "", password = "" }
+
+        local m, err = ngx.re.match(auth, "Basic\\s(.+)")
+        if err then
+            -- error authorization
+            return nil, err
+        end
+
+        local decoded = ngx.decode_base64(m[1])
+
+        local res
+        res, err = ngx_re.split(decoded, ":")
+        if err then
+            return nil, "split authorization err:" .. err
+        end
+
+        obj.username = ngx.re.gsub(res[1], "\\s+", "")
+        obj.password = ngx.re.gsub(res[2], "\\s+", "")
+        core.log.info("plugin access phase, authorization: ", obj.username, ": ", obj.password)
+
+        return obj, nil
+    end
+
+    local matcher, err = lrucache(authorization, nil, do_extract, authorization)
+
+    if matcher then
+        return matcher.username, matcher.password, err
+    else
+        return "", "", err
+    end
+
+end
+
+local create_consume_cache
+do
+    local consumer_ids = {}
+
+    function create_consume_cache(consumers)
+        core.table.clear(consumer_ids)
+
+        for _, cur_consumer in ipairs(consumers.nodes) do
+            core.log.info("consumer node: ", core.json.delay_encode(cur_consumer))
+            consumer_ids[cur_consumer.auth_conf.username] = cur_consumer
+        end
+
+        return consumer_ids
+    end
+end
+
+function _M.access(conf, ctx)
+    core.log.info("plugin access phase, conf: ", core.json.delay_encode(conf))
+
+    -- 1. extract authorization from header
+    local auth_header = core.request.header(ctx, "Authorization")
+    if not auth_header then
+        core.response.set_header("WWW-Authenticate", "Basic realm='.'")
+        return 401, { message = "Missing authorization in request" }
+    end
+
+    local username, password, err = extract_auth_header(auth_header)
+    if err then
+        return 401, { message = err }
+    end
+
+    -- 2. get user info from consumer plugin
+    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)
+
+    -- 3. check user exists
+    local cur_consumer = consumers[username]
+    if not cur_consumer then
+        return 401, { message = "Invalid user key in authorization" }
+    end
+    core.log.info("consumer: ", core.json.delay_encode(cur_consumer))
+
+
+    -- 4. check the password is correct
+    if cur_consumer.auth_conf.password ~= password then
+        return 401, { message = "Password is error" }
+    end
+
+    ctx.consumer = cur_consumer
+    ctx.consumer_id = cur_consumer.consumer_id
+
+    core.log.info("hit basic-auth access")
+end
+
+return _M
diff --git a/t/admin/plugins.t b/t/admin/plugins.t
index 5301619..eae4a6d 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","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect","response-rewrite"\]/
+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"\]/
 --- no_error_log
 [error]
diff --git a/t/debug/debug-mode.t b/t/debug/debug-mode.t
index a67a01c..3e18a0f 100644
--- a/t/debug/debug-mode.t
+++ b/t/debug/debug-mode.t
@@ -58,6 +58,7 @@ qr/loaded plugin and sort by priority: [-\d]+ name: [\w-]+/
 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: 2520 name: basic-auth
 loaded plugin and sort by priority: 2510 name: jwt-auth
 loaded plugin and sort by priority: 2500 name: key-auth
 loaded plugin and sort by priority: 1008 name: proxy-rewrite
diff --git a/t/plugin/basic-auth.t b/t/plugin/basic-auth.t
new file mode 100644
index 0000000..4a755dd
--- /dev/null
+++ b/t/plugin/basic-auth.t
@@ -0,0 +1,199 @@
+#
+# 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(2);
+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.basic-auth")
+            local ok, err = plugin.check_schema({username = 'foo', password = 'bar'})
+            if not ok then
+                ngx.say(err)
+            end
+
+            ngx.say("done")
+        }
+    }
+--- request
+GET /t
+--- response_body
+done
+--- no_error_log
+[error]
+
+
+
+=== TEST 2: wrong type of string
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.basic-auth")
+            local ok, err = plugin.check_schema({username = 123, password = "bar"})
+            if not ok then
+                ngx.say(err)
+            end
+
+            ngx.say("done")
+        }
+    }
+--- request
+GET /t
+--- response_body
+property "username" 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": "foo",
+                    "plugins": {
+                        "basic-auth": {
+                            "username": "foo",
+                            "password": "bar"
+                        }
+                    }
+                }]],
+                [[{
+                    "node": {
+                        "value": {
+                            "username": "foo",
+                            "plugins": {
+                                "basic-auth": {
+                                    "username": "foo",
+                                    "password": "bar"
+                                }
+                            }
+                        }
+                    },
+                    "action": "set"
+                }]]
+                )
+
+            ngx.status = code
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 4: enable basic auth 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": {
+                        "basic-auth": {}
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 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: verify, missing authorization
+--- request
+GET /hello
+--- error_code: 401
+--- response_body
+{"message":"Missing authorization in request"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 6: verify, invalid username
+--- request
+GET /hello
+--- more_headers
+Authorization: Basic YmFyOmJhcgo=
+--- error_code: 401
+--- response_body
+{"message":"Invalid user key in authorization"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 7: verify, invalid password
+--- request
+GET /hello
+--- more_headers
+Authorization: Basic Zm9vOmZvbwo=
+--- error_code: 401
+--- response_body
+{"message":"Password is error"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 8: verify
+--- request
+GET /hello
+--- more_headers
+Authorization: Basic Zm9vOmJhcg==
+--- response_body
+hello world
+--- no_error_log
+[error]
+