feat: use interceptors to protect plugin's route (#2416)

This commit is contained in:
罗泽轩 2020-10-17 17:45:26 +08:00 committed by GitHub
parent 90109ca678
commit 6a543a98a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 422 additions and 41 deletions

View File

@ -14,9 +14,11 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local error = error
local pcall = pcall
local require = require
local core = require("apisix.core")
local api_router = require("apisix.api_router")
local _M = {
}
@ -44,15 +46,26 @@ local function check_conf(plugin_name, conf)
return nil, {error_msg = "invalid plugin name"}
end
local schema = plugin_object.metadata_schema
if not schema then
return nil, {error_msg = "no metadata schema for plugin " .. plugin_name}
end
if not conf then
return nil, {error_msg = "missing configurations"}
end
local schema = plugin_object.metadata_schema or {
type = "object",
properties = {},
}
if not schema.properties then
schema.properties = {
additionalProperties = false,
}
end
-- inject interceptors schema to each plugins
if schema.properties.interceptors then
error("'interceptors' can not be used as the name of metadata schema's field")
end
schema.properties.interceptors = api_router.interceptors_schema
core.log.info("schema: ", core.json.delay_encode(schema))
core.log.info("conf : ", core.json.delay_encode(conf))
local ok, err = core.schema.check(schema, conf)

View File

@ -16,13 +16,57 @@
--
local require = require
local router = require("resty.radixtree")
local plugin = require("apisix.plugin")
local plugin_mod = require("apisix.plugin")
local ip_restriction = require("apisix.plugins.ip-restriction")
local core = require("apisix.core")
local ipairs = ipairs
local _M = {}
local match_opts = {}
local interceptors = {
["ip-restriction"] = {
run = function (conf, ctx)
return ip_restriction.access(conf, ctx)
end,
schema = ip_restriction.schema,
}
}
_M.interceptors_schema = {
type = "array",
items = {
type = "object",
minItems = 1,
properties = {
name = {
type = "string",
enum = {"ip-restriction"},
},
conf = {
type = "object",
}
},
required = {"name", "conf"},
dependencies = {
name = {
oneOf = {}
}
}
}
}
for name, attrs in pairs(interceptors) do
core.table.insert(_M.interceptors_schema.items.properties.name.enum, name)
core.table.insert(_M.interceptors_schema.items.dependencies.name.oneOf, {
properties = {
name = {
enum = {name},
},
conf = attrs.schema,
}
})
end
local fetch_api_router
@ -31,9 +75,10 @@ do
function fetch_api_router()
core.table.clear(routes)
for _, plugin in ipairs(plugin.plugins) do
for _, plugin in ipairs(plugin_mod.plugins) do
local api_fun = plugin.api
if api_fun then
local name = plugin.name
local api_routes = api_fun()
core.log.debug("fetched api routes: ",
core.json.delay_encode(api_routes, true))
@ -41,8 +86,25 @@ function fetch_api_router()
core.table.insert(routes, {
methods = route.methods,
paths = route.uri,
handler = function (...)
local code, body = route.handler(...)
handler = function (api_ctx)
local code, body
local metadata = plugin_mod.plugin_metadata(name)
if metadata and metadata.interceptors then
for _, rule in ipairs(metadata.interceptors) do
local f = interceptors[rule.name]
if f == nil then
core.log.error("unknown interceptor: ", rule.name)
else
code, body = f.run(rule.conf, api_ctx)
if code or body then
return core.response.exit(code, body)
end
end
end
end
code, body = route.handler(api_ctx)
if code or body then
core.response.exit(code, body)
end
@ -59,7 +121,7 @@ end -- do
function _M.match(api_ctx)
local api_router = core.lrucache.global("api_router", plugin.load_times, fetch_api_router)
local api_router = core.lrucache.global("api_router", plugin_mod.load_times, fetch_api_router)
if not api_router then
core.log.error("failed to fetch valid api router")
return false

View File

@ -0,0 +1,55 @@
<!--
#
# 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](zh-cn/plugin-interceptors.md)
## Plugin interceptors
Some plugins will register API to serve their purposes.
Since these API are not added as regular [Route](admin-api.md), we can't add
plugins to protect them. To solve the problem, we add a new concept called 'interceptors'
to run rules to protect them.
Here is an example to limit the access of `/apisix/prometheus/metrics` (a route introduced via plugin prometheus)
to clients in `10.0.0.0/24`:
```shell
$ curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/prometheus -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -X PUT -d '
{
"interceptors": [
{
"name": "ip-restriction",
"conf": {
"whitelist": ["10.0.0.0/24"]
}
}
]
}
```
You can see that the interceptors are configured like the plugins. The `name` is
the name of plugin which you want to run and the `conf` is the configuration of the
plugin.
Currently we only support a subset of plugins which can be run as interceptors.
Supported interceptors:
* [ip-restriction](./plugins/ip-restriction.md)

View File

@ -40,6 +40,11 @@
None
## API
This plugin will add `/apisix/batch-requests` as the endpoint.
You may need to use [interceptors](plugin-interceptors.md) to protect it.
## How To Enable
Default enabled

View File

@ -45,6 +45,11 @@ For more information on JWT, refer to [JWT](https://jwt.io/) for more informatio
| exp | integer | optional | 86400 | [1,...] | token's expire time, in seconds |
| base64_secret | boolean | optional | false | | whether secret is base64 encoded |
## API
This plugin will add `/apisix/plugin/jwt/sign` to sign.
You may need to use [interceptors](plugin-interceptors.md) to protect it.
## How To Enable
1. set a consumer and config the value of the `jwt-auth` option

View File

@ -27,6 +27,11 @@ This plugin exposes metrics in Prometheus Exposition format.
none.
## API
This plugin will add `/apisix/prometheus/metrics` to expose the metrics.
You may need to use [interceptors](plugin-interceptors.md) to protect it.
## How to enable it
`prometheus` plugin can be enable with empty table, because it doesn't have

View File

@ -42,6 +42,15 @@ The rbac feature is provided by [wolf](https://github.com/iGeeky/wolf). For more
| appid | string | optional | "unset" | | Set the app id. The app id must be added in wolf-console. |
| header_prefix | string | optional | "X-" | | prefix of custom HTTP header. After authentication is successful, three headers will be added to the request header (for backend) and response header (for frontend): `X-UserId`, `X-Username`, `X-Nickname`. |
## API
This plugin will add several API:
* /apisix/plugin/wolf-rbac/login
* /apisix/plugin/wolf-rbac/change_pwd
* /apisix/plugin/wolf-rbac/user_info
You may need to use [interceptors](plugin-interceptors.md) to protect it.
## Dependencies

View File

@ -0,0 +1,50 @@
<!--
#
# 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](../plugin-interceptors.md)
## Plugin interceptors
有些插件为实现它的功能会注册额外的接口。
由于这些接口不是通过 admin API 添加的,所以没办法像管理 Route 那样管理它们。为了能够保护这些接口不被利用,我们引入了 interceptors 的概念。
下面是通过 interceptors 来保护由 prometheus 插件引入的 `/apisix/prometheus/metrics` 接口,限定只能由 `10.0.0.0/24` 网段的用户访问:
```shell
$ curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/prometheus -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -X PUT -d '
{
"interceptors": [
{
"name": "ip-restriction",
"conf": {
"whitelist": ["10.0.0.0/24"]
}
}
]
}
```
我们能看到配置 interceptors 就像配置 plugin 一样name 是 interceptor 的名称,而 conf 是它的配置。
当前我们只支持一部分插件作为 interceptor 运行。
支持的 interceptor
* [ip-restriction](./plugins/ip-restriction.md)

View File

@ -40,6 +40,11 @@
## 接口
插件会增加 `/apisix/batch-requests` 这个接口,你可能需要通过 [interceptors](plugin-interceptors.md)
来保护它。
## 如何启用
本插件默认启用。

View File

@ -46,6 +46,11 @@
| exp | integer | 可选 | 86400 | [1,...] | token 的超时时间 |
| base64_secret | boolean | 可选 | false | | 密钥是否为 base64 编码 |
## 接口
插件会增加 `/apisix/plugin/jwt/sign` 这个接口,你可能需要通过 [interceptors](plugin-interceptors.md)
来保护它。
## 如何启用
1. 创建一个 consumer 对象,并设置插件 `jwt-auth` 的值。

View File

@ -27,6 +27,11 @@
## 接口
插件会增加 `/apisix/prometheus/metrics` 这个接口,你可能需要通过 [interceptors](plugin-interceptors.md)
来保护它。
## 如何开启插件
`prometheus` 插件用空{}就可以开启了,他没有任何的选项。

View File

@ -42,6 +42,16 @@ rbac功能由[wolf](https://github.com/iGeeky/wolf)提供, 有关 `wolf` 的更
| appid | string | 可选 | "unset" | | 设置应用id, 该应用id, 需要是在 `wolf-console` 中已经添加的应用id |
| header_prefix | string | 可选 | "X-" | | 自定义http头的前缀。`wolf-rbac`在鉴权成功后, 会在请求头(用于传给后端)及响应头(用于传给前端)中添加3个头: `X-UserId`, `X-Username`, `X-Nickname` |
## 接口
插件会增加这些接口:
* /apisix/plugin/wolf-rbac/login
* /apisix/plugin/wolf-rbac/change_pwd
* /apisix/plugin/wolf-rbac/user_info
你可能需要通过 [interceptors](plugin-interceptors.md) 来保护它们。
## 依赖项
### 安装 wolf, 并启动服务

View File

@ -237,37 +237,7 @@ GET /t
=== TEST 8: no plugin metadata schema
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/plugin_metadata/echo',
ngx.HTTP_PUT,
[[{"k": "v"}]],
[[{
"node": {
"value": "sdf"
},
"action": "set"
}]]
)
ngx.status = code
ngx.print(body)
}
}
--- request
GET /t
--- error_code: 400
--- response_body
{"error_msg":"no metadata schema for plugin echo"}
--- no_error_log
[error]
=== TEST 9: verify metadata schema fail
=== TEST 8: verify metadata schema fail
--- config
location /t {
content_by_lua_block {
@ -299,3 +269,185 @@ GET /t
qr/\{"error_msg":"invalid configuration: property \\"ikey\\" is required"\}/
--- no_error_log
[error]
=== TEST 9: set plugin interceptors
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/plugin_metadata/prometheus',
ngx.HTTP_PUT,
[[{
"interceptors": [
{
"name": "ip-restriction",
"conf": {
"whitelist": ["192.168.1.0/24"]
}
}
]
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- request
GET /t
--- response_body
passed
--- no_error_log
[error]
=== TEST 10: hit prometheus route
--- request
GET /apisix/prometheus/metrics
-- error_code: 403
=== TEST 11: set plugin interceptors (allow ip access)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/plugin_metadata/prometheus',
ngx.HTTP_PUT,
[[{
"interceptors": [
{
"name": "ip-restriction",
"conf": {
"whitelist": ["127.0.0.1"]
}
}
]
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- request
GET /t
--- response_body
passed
--- no_error_log
[error]
=== TEST 12: hit prometheus route again
--- request
GET /apisix/prometheus/metrics
-- error_code: 200
=== TEST 13: invalid interceptors configure (unknown interceptor)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/plugin_metadata/prometheus',
ngx.HTTP_PUT,
[[{
"interceptors": [
{
"name": "unknown",
"conf": {
"whitelist": ["127.0.0.1"]
}
}
]
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- request
GET /t
--- response_body eval
qr/\{"error_msg":"invalid configuration: property \\"interceptors\\" validation failed: failed to validate item 1: property \\"name\\" validation failed: matches non of the enum values"\}/
--- error_code: 400
--- no_error_log
[error]
=== TEST 14: invalid interceptors configure (missing conf)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/plugin_metadata/prometheus',
ngx.HTTP_PUT,
[[{
"interceptors": [
{
"name": "ip-restriction"
}
]
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- request
GET /t
--- error_code: 400
--- response_body eval
qr/\{"error_msg":"invalid configuration: property \\"interceptors\\" validation failed: failed to validate item 1: property \\"conf\\" is required"\}/
--- no_error_log
[error]
=== TEST 15: invalid interceptors configure (invalid interceptor configure)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/plugin_metadata/prometheus',
ngx.HTTP_PUT,
[[{
"interceptors": [
{
"name": "ip-restriction",
"conf": {"aa": "b"}
}
]
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- request
GET /t
--- error_code: 400
--- response_body eval
qr/\{"error_msg":"invalid configuration: property \\"interceptors\\" validation failed: failed to validate item 1: failed to validate dependent schema for \\"name\\": value should match only one schema, but matches none"\}/
--- no_error_log
[error]