This is an automated email from the ASF dual-hosted git repository. membphis pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push: new 0eacf3a feat: improve jsonschema validation on route resource (#3008) 0eacf3a is described below commit 0eacf3af7dd12d5cd7ba1e2d8e9b4ba1833218b1 Author: 罗泽轩 <spacewander...@gmail.com> AuthorDate: Thu Dec 10 22:53:53 2020 +0800 feat: improve jsonschema validation on route resource (#3008) fix #2671 fix #2991 --- apisix/admin/routes.lua | 9 +-- apisix/schema_def.lua | 35 ++++++++++ doc/admin-api.md | 11 ++-- doc/zh-cn/admin-api.md | 6 +- t/admin/routes2.t | 169 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 218 insertions(+), 12 deletions(-) diff --git a/apisix/admin/routes.lua b/apisix/admin/routes.lua index f54a090..e5b2cb1 100644 --- a/apisix/admin/routes.lua +++ b/apisix/admin/routes.lua @@ -51,10 +51,6 @@ local function check_conf(id, conf, need_id) core.log.info("schema: ", core.json.delay_encode(core.schema.route)) core.log.info("conf : ", core.json.delay_encode(conf)) - local ok, err = core.schema.check(core.schema.route, conf) - if not ok then - return nil, {error_msg = "invalid configuration: " .. err} - end if conf.host and conf.hosts then return nil, {error_msg = "only one of host or hosts is allowed"} @@ -65,6 +61,11 @@ local function check_conf(id, conf, need_id) .. "allowed"} end + local ok, err = core.schema.check(core.schema.route, conf) + if not ok then + return nil, {error_msg = "invalid configuration: " .. err} + end + local upstream_conf = conf.upstream if upstream_conf then local ok, err = upstreams.check_upstream_conf(upstream_conf) diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua index 57f0202..aaa9959 100644 --- a/apisix/schema_def.lua +++ b/apisix/schema_def.lua @@ -421,6 +421,7 @@ _M.route = { description = "HTTP uri", type = "string", }, + minItems = 1, uniqueItems = true, }, name = rule_name_def, @@ -441,12 +442,14 @@ _M.route = { hosts = { type = "array", items = host_def, + minItems = 1, uniqueItems = true, }, remote_addr = remote_addr_def, remote_addrs = { type = "array", items = remote_addr_def, + minItems = 1, uniqueItems = true, }, vars = { @@ -504,6 +507,38 @@ _M.route = { default = 1 }, }, + allOf = { + { + oneOf = { + {required = {"uri"}}, + {required = {"uris"}}, + }, + }, + { + oneOf = { + {["not"] = { + anyOf = { + {required = {"host"}}, + {required = {"hosts"}}, + } + }}, + {required = {"host"}}, + {required = {"hosts"}} + }, + }, + { + oneOf = { + {["not"] = { + anyOf = { + {required = {"remote_addr"}}, + {required = {"remote_addrs"}}, + } + }}, + {required = {"remote_addr"}}, + {required = {"remote_addrs"}} + }, + }, + }, anyOf = { {required = {"plugins", "uri"}}, {required = {"upstream", "uri"}}, diff --git a/doc/admin-api.md b/doc/admin-api.md index ba9f5df..7085b2d 100644 --- a/doc/admin-api.md +++ b/doc/admin-api.md @@ -55,11 +55,12 @@ |---------|---------|----|-----------|----| |name |False |Auxiliary |Identifies route names.|customer-xxxx| |desc |False |Auxiliary |route description, usage scenarios, and more.|customer xxxx| -|uri |True |Match Rules|In addition to full matching such as `/foo/bar`、`/foo/gloo`, using different [Router](architecture-design.md#router) allows more advanced matching, see [Router](architecture-design.md#router) for more.|"/hello"| -|host |False |Match Rules|Currently requesting a domain name, such as `foo.com`; pan-domain names such as `*.foo.com` are also supported.|"foo.com"| -|hosts |False |Match Rules|The `host` in the form of a list means that multiple different hosts are allowed, and match any one of them.|{"foo.com", "*.bar.com"}| -|remote_addr|False |Match Rules|The client requests an IP address: `192.168.1.101`, `192.168.1.102`, and CIDR format support `192.168.1.0/24`. In particular, APISIX also fully supports IPv6 address matching: `::1`, `fe80::1`, `fe80::1/64`, etc.|"192.168.1.0/24"| -|remote_addrs|False |Match Rules|The `remote_addr` in the form of a list indicates that multiple different IP addresses are allowed, and match any one of them.|{"127.0.0.1", "192.0.0.0/8", "::1"}| +|uri |True, can't be used with `uris` |Match Rules|In addition to full matching such as `/foo/bar`、`/foo/gloo`, using different [Router](architecture-design.md#router) allows more advanced matching, see [Router](architecture-design.md#router) for more.|"/hello"| +|uris |True, can't be used with `uri`|Match Rules|The `uri` in the form of a non-empty list means that multiple different uris are allowed, and match any one of them.|["/hello", "/word"]| +|host |False, can't be used with `hosts` |Match Rules|Currently requesting a domain name, such as `foo.com`; PAN domain names such as `*.foo.com` are also supported.|"foo.com"| +|hosts |False, can't be used with `host` |Match Rules|The `host` in the form of a non-empty list means that multiple different hosts are allowed, and match any one of them.|{"foo.com", "*.bar.com"}| +|remote_addr|False, can't be used with `remote_addrs` |Match Rules|The client requests an IP address: `192.168.1.101`, `192.168.1.102`, and CIDR format support `192.168.1.0/24`. In particular, APISIX also fully supports IPv6 address matching: `::1`, `fe80::1`, `fe80::1/64`, etc.|"192.168.1.0/24"| +|remote_addrs|False, can't be used with `remote_addr` |Match Rules|The `remote_addr` in the form of a non-empty list indicates that multiple different IP addresses are allowed, and match any one of them.|{"127.0.0.1", "192.0.0.0/8", "::1"}| |methods |False |Match Rules|If empty or without this option, there are no `method` restrictions, and it can be a combination of one or more: `GET`,`POST`,`PUT`,`DELETE`,`PATCH`, `HEAD`,`OPTIONS`,`CONNECT`,`TRACE`.|{"GET", "POST"}| |priority |False |Match Rules|If different routes contain the same `uri`, determine which route is matched first based on the attribute` priority`. Larger value means higher priority. The default value is 0.|priority = 10| |vars |False |Match Rules |A list of one or more `{var, operator, val}` elements, like this: `{{var, operator, val}, {var, operator, val}, ...}}`. For example: `{"arg_name", "==", "json"}` means that the current request parameter `name` is `json`. The `var` here is consistent with the internal variable name of Nginx, so you can also use `request_uri`, `host`, etc. For the operator part, the currently supported operators are `==`, `~=`,`>`, `<`, and `~~`. For the `>` and `<` operat [...] diff --git a/doc/zh-cn/admin-api.md b/doc/zh-cn/admin-api.md index 0710345..de519b2 100644 --- a/doc/zh-cn/admin-api.md +++ b/doc/zh-cn/admin-api.md @@ -57,7 +57,7 @@ |名字 |可选项 |类型 |说明 |示例| |---------|---------|----|-----------|----| |uri |与 `uris` 二选一 |匹配规则|除了如 `/foo/bar`、`/foo/gloo` 这种全量匹配外,使用不同 [Router](architecture-design.md#router) 还允许更高级匹配,更多见 [Router](architecture-design.md#router)。|"/hello"| -|uris |与 `uri` 二选一 |匹配规则|数组形式,可以匹配多个 `uri`|["/hello", "/world"]| +|uris |与 `uri` 二选一 |匹配规则|非空数组形式,可以匹配多个 `uri`|["/hello", "/world"]| |plugins |`plugins`、`script`、`upstream`/`upstream_id`、`service_id`至少选择一个 |Plugin|详见 [Plugin](architecture-design.md#plugin) || |script |`plugins`、`script`、`upstream`/`upstream_id`、`service_id`至少选择一个 |Script|详见 [Script](architecture-design.md#script) || |upstream |`plugins`、`script`、`upstream`/`upstream_id`、`service_id`至少选择一个 |Upstream|启用的 Upstream 配置,详见 [Upstream](architecture-design.md#upstream)|| @@ -67,9 +67,9 @@ |name |可选 |辅助 |标识路由名称|route-xxxx| |desc |可选 |辅助 |标识描述、使用场景等。|客户 xxxx| |host |可选 |匹配规则|当前请求域名,比如 `foo.com`;也支持泛域名,比如 `*.foo.com`。|"foo.com"| -|hosts |可选 |匹配规则|列表形态的 `host`,表示允许有多个不同 `host`,匹配其中任意一个即可。|{"foo.com", "*.bar.com"}| +|hosts |可选 |匹配规则|非空列表形态的 `host`,表示允许有多个不同 `host`,匹配其中任意一个即可。|{"foo.com", "*.bar.com"}| |remote_addr|可选 |匹配规则|客户端请求 IP 地址: `192.168.1.101`、`192.168.1.102` 以及 CIDR 格式的支持 `192.168.1.0/24`。特别的,APISIX 也完整支持 IPv6 地址匹配:`::1`,`fe80::1`, `fe80::1/64` 等。|"192.168.1.0/24"| -|remote_addrs|可选 |匹配规则|列表形态的 `remote_addr`,表示允许有多个不同 IP 地址,符合其中任意一个即可。|{"127.0.0.1", "192.0.0.0/8", "::1"}| +|remote_addrs|可选 |匹配规则|非空列表形态的 `remote_addr`,表示允许有多个不同 IP 地址,符合其中任意一个即可。|{"127.0.0.1", "192.0.0.0/8", "::1"}| |methods |可选 |匹配规则|如果为空或没有该选项,代表没有任何 `method` 限制,也可以是一个或多个的组合:`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`,`CONNECT`,`TRACE`。|{"GET", "POST"}| |priority |可选 |匹配规则|如果不同路由包含相同 `uri`,根据属性 `priority` 确定哪个 `route` 被优先匹配,值越大优先级越高,默认值为 0。|priority = 10| |vars |可选 |匹配规则|由一个或多个`{var, operator, val}`元素组成的列表,类似这样:`{{var, operator, val}, {var, operator, val}, ...}}`。例如:`{"arg_name", "==", "json"}`,表示当前请求参数 `name` 是 `json`。这里的 `var` 与 Nginx 内部自身变量命名是保持一致,所以也可以使用 `request_uri`、`host` 等;对于 `operator` 部分,目前已支持的运算符有 `==`、`~=`、`>`、`<` 和 `~~`。对于`>`和`<`两个运算符,会把结果先转换成 number 然后再做比较。查看支持的[运算符列表](#运算符列表)|{{"arg_name", "==", "json"}, {"arg_age", ">", 18}}| diff --git a/t/admin/routes2.t b/t/admin/routes2.t index d4b77f6..7357250 100644 --- a/t/admin/routes2.t +++ b/t/admin/routes2.t @@ -349,3 +349,172 @@ GET /t {"action":"delete","deleted":"1","key":"/apisix/routes/1","node":{}} --- no_error_log [error] + + + +=== TEST 10: invalid route: empty remote_addrs +--- 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, + [[{ + "methods": ["GET"], + "remote_addrs": [], + "upstream": { + "nodes": { + "127.0.0.1:8080": 1 + }, + "type": "roundrobin" + }, + "uri": "/index.html" + }]] + ) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body_like eval +qr/property \\"remote_addrs\\" validation failed:/ +--- no_error_log +[error] + + + +=== TEST 11: invalid route: empty uris +--- 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, + [[{ + "methods": ["GET"], + "upstream": { + "nodes": { + "127.0.0.1:8080": 1 + }, + "type": "roundrobin" + }, + "uris": [] + }]] + ) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body_like eval +qr/property \\"uris\\" validation failed:/ +--- no_error_log +[error] + + + +=== TEST 12: invalid route: empty hosts +--- 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, + [[{ + "methods": ["GET"], + "upstream": { + "nodes": { + "127.0.0.1:8080": 1 + }, + "type": "roundrobin" + }, + "hosts": [], + "uri": "/" + }]] + ) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body_like eval +qr/property \\"hosts\\" validation failed:/ +--- no_error_log +[error] + + + +=== TEST 13: invalid route: uris & uri +--- 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, + [[{ + "methods": ["GET"], + "upstream": { + "nodes": { + "127.0.0.1:8080": 1 + }, + "type": "roundrobin" + }, + "uris": ["/"], + "uri": "/" + }]] + ) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body_like eval +qr/value should match only one schema/ +--- no_error_log +[error] + + + +=== TEST 14: enable remote_addrs and remote_addr together +--- 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, + [[{ + "uri": "/index.html", + "upstream": { + "nodes": { + "127.0.0.1:8080": 1 + }, + "type": "roundrobin" + }, + "remote_addr": "127.0.0.1", + "remote_addrs": ["127.0.0.1"] + }]] + ) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body +{"error_msg":"only one of remote_addr or remote_addrs is allowed"} +--- no_error_log +[error]