You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by sp...@apache.org on 2022/03/13 11:31:54 UTC
[apisix] branch master updated: feat: set cors allow origins by plugin metadata (#6546)
This is an automated email from the ASF dual-hosted git repository.
spacewander 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 09e36cd feat: set cors allow origins by plugin metadata (#6546)
09e36cd is described below
commit 09e36cd7aecc1609bfd6742be0ab3847838274e8
Author: Gary-Airwallex <ga...@airwallex.com>
AuthorDate: Sun Mar 13 19:31:46 2022 +0800
feat: set cors allow origins by plugin metadata (#6546)
Co-authored-by: tzssangglass <tz...@gmail.com>
---
apisix/plugins/cors.lua | 85 ++++++++--
docs/en/latest/plugins/cors.md | 7 +
docs/zh/latest/plugins/cors.md | 7 +
t/plugin/cors3.t | 353 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 442 insertions(+), 10 deletions(-)
diff --git a/apisix/plugins/cors.lua b/apisix/plugins/cors.lua
index 6375173..f0d911f 100644
--- a/apisix/plugins/cors.lua
+++ b/apisix/plugins/cors.lua
@@ -15,6 +15,7 @@
-- limitations under the License.
--
local core = require("apisix.core")
+local plugin = require("apisix.plugin")
local ngx = ngx
local plugin_name = "cors"
local str_find = core.string.find
@@ -22,12 +23,26 @@ local re_gmatch = ngx.re.gmatch
local re_compile = require("resty.core.regex").re_match_compile
local re_find = ngx.re.find
local ipairs = ipairs
+local origins_pattern = [[^(\*|\*\*|null|\w+://[^,]+(,\w+://[^,]+)*)$]]
local lrucache = core.lrucache.new({
type = "plugin",
})
+local metadata_schema = {
+ type = "object",
+ properties = {
+ allow_origins = {
+ type = "object",
+ additionalProperties = {
+ type = "string",
+ pattern = origins_pattern
+ }
+ },
+ },
+}
+
local schema = {
type = "object",
properties = {
@@ -37,7 +52,7 @@ local schema = {
"'**' to allow forcefully(it will bring some security risks, be carefully)," ..
"multiple origin use ',' to split. default: *.",
type = "string",
- pattern = [[^(\*|\*\*|null|\w+://[^,]+(,\w+://[^,]+)*)$]],
+ pattern = origins_pattern,
default = "*"
},
allow_methods = {
@@ -92,6 +107,18 @@ local schema = {
minItems = 1,
uniqueItems = true,
},
+ allow_origins_by_metadata = {
+ type = "array",
+ description =
+ "set allowed origins by referencing origins in plugin metadata",
+ items = {
+ type = "string",
+ minLength = 1,
+ maxLength = 4096,
+ },
+ minItems = 1,
+ uniqueItems = true,
+ },
}
}
@@ -100,15 +127,16 @@ local _M = {
priority = 4000,
name = plugin_name,
schema = schema,
+ metadata_schema = metadata_schema,
}
-local function create_multiple_origin_cache(conf)
- if not str_find(conf.allow_origins, ",") then
+local function create_multiple_origin_cache(allow_origins)
+ if not str_find(allow_origins, ",") then
return nil
end
local origin_cache = {}
- local iterator, err = re_gmatch(conf.allow_origins, "([^,]+)", "jiox")
+ local iterator, err = re_gmatch(allow_origins, "([^,]+)", "jiox")
if not iterator then
core.log.error("match origins failed: ", err)
return nil
@@ -128,7 +156,10 @@ local function create_multiple_origin_cache(conf)
end
-function _M.check_schema(conf)
+function _M.check_schema(conf, schema_type)
+ if schema_type == core.schema.TYPE_METADATA then
+ return core.schema.check(metadata_schema, conf)
+ end
local ok, err = core.schema.check(schema, conf)
if not ok then
return false, err
@@ -177,13 +208,23 @@ local function set_cors_headers(conf, ctx)
end
end
-local function process_with_allow_origins(conf, ctx, req_origin)
- local allow_origins = conf.allow_origins
+local function process_with_allow_origins(allow_origins, ctx, req_origin,
+ cache_key, cache_version)
if allow_origins == "**" then
allow_origins = req_origin or '*'
end
- local multiple_origin, err = core.lrucache.plugin_ctx(lrucache, ctx, nil,
- create_multiple_origin_cache, conf)
+
+ local multiple_origin, err
+ if cache_key and cache_version then
+ multiple_origin, err = lrucache(
+ cache_key, cache_version, create_multiple_origin_cache, allow_origins
+ )
+ else
+ multiple_origin, err = core.lrucache.plugin_ctx(
+ lrucache, ctx, nil, create_multiple_origin_cache, allow_origins
+ )
+ end
+
if err then
return 500, {message = "get multiple origin cache failed: " .. err}
end
@@ -225,6 +266,25 @@ local function match_origins(req_origin, allow_origins)
return req_origin == allow_origins or allow_origins == '*'
end
+local function process_with_allow_origins_by_metadata(allow_origins_by_metadata, ctx, req_origin)
+ if allow_origins_by_metadata == nil then
+ return
+ end
+
+ local metadata = plugin.plugin_metadata(plugin_name)
+ if metadata and metadata.value.allow_origins then
+ local allow_origins_map = metadata.value.allow_origins
+ for _, key in ipairs(allow_origins_by_metadata) do
+ local allow_origins_conf = allow_origins_map[key]
+ local allow_origins = process_with_allow_origins(allow_origins_conf, ctx, req_origin,
+ plugin_name .. "#" .. key, metadata.modifiedIndex)
+ if match_origins(req_origin, allow_origins) then
+ return req_origin
+ end
+ end
+ end
+end
+
function _M.rewrite(conf, ctx)
-- save the original request origin as it may be changed at other phase
@@ -239,10 +299,15 @@ function _M.header_filter(conf, ctx)
local req_origin = ctx.original_request_origin
-- Try allow_origins first, if mismatched, try allow_origins_by_regex.
local allow_origins
- allow_origins = process_with_allow_origins(conf, ctx, req_origin)
+ allow_origins = process_with_allow_origins(conf.allow_origins, ctx, req_origin)
if not match_origins(req_origin, allow_origins) then
allow_origins = process_with_allow_origins_by_regex(conf, ctx, req_origin)
end
+ if not allow_origins then
+ allow_origins = process_with_allow_origins_by_metadata(
+ conf.allow_origins_by_metadata, ctx, req_origin
+ )
+ end
if allow_origins then
ctx.cors_allow_origins = allow_origins
set_cors_headers(conf, ctx)
diff --git a/docs/en/latest/plugins/cors.md b/docs/en/latest/plugins/cors.md
index 6b92a54..2c332cf 100644
--- a/docs/en/latest/plugins/cors.md
+++ b/docs/en/latest/plugins/cors.md
@@ -36,12 +36,19 @@ title: cors
| max_age | integer | optional | 5 | | Maximum number of seconds the results can be cached. Within this time range, the browser will reuse the last check result. `-1` means no cache. Please note that the maximum value is depended on browser, please refer to [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age#Directives) for details. |
| allow_credential | boolean | optional | false | | Enable request include credential (such as Cookie etc.). According to CORS specification, if you set this option to `true`, you can not use '*' for other options. |
| allow_origins_by_regex | array | optional | nil | | Use regex expressions to match which origin is allowed to enable CORS, for example, [".*\.test.com"] can use to match all subdomain of test.com |
+| allow_origins_by_metadata | array | optional | nil | | Match which origin is allowed to enable CORS by referencing `allow_origins` set in plugin metadata. For example, if `"allow_origins": {"EXAMPLE": "https://example.com"}` is set in metadata, then `["EXAMPLE"]` can be used to match the origin `https://example.com` |
> **Tips**
>
> Please note that `allow_credential` is a very sensitive option, so choose to enable it carefully. After set it be `true`, the default `*` of other parameters will be invalid, you must specify their values explicitly.
> When using `**`, you must fully understand that it introduces some security risks, such as CSRF, so make sure that this security level meets your expectations before using it。
+## Metadata
+
+| Name | Type | Requirement | Default | Valid | Description |
+| ----------- | ------ | ----------- | ------- | ----- | ---------------------------------------------------------------------- |
+| allow_origins | object | optional | | | A map from origin reference to allowed origins; its key is the reference used by `allow_origins_by_metadata` and its value is a string equivalent to `allow_origins` in plugin attributes |
+
## How To Enable
Create a `Route` or `Service` object and configure `cors` plugin.
diff --git a/docs/zh/latest/plugins/cors.md b/docs/zh/latest/plugins/cors.md
index f2c89bd..e3cee9d 100644
--- a/docs/zh/latest/plugins/cors.md
+++ b/docs/zh/latest/plugins/cors.md
@@ -36,12 +36,19 @@ title: cors
| max_age | integer | 可选 | 5 | | 浏览器缓存 CORS 结果的最大时间,单位为秒,在这个时间范围内浏览器会复用上一次的检查结果,`-1` 表示不缓存。请注意各个浏览器允许的最大时间不同,详情请参考 [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age#Directives)。 |
| allow_credential | boolean | 可选 | false | | 是否允许跨域访问的请求方携带凭据(如 Cookie 等)。根据 CORS 规范,如果设置该选项为 `true`,那么将不能在其他选项中使用 `*`。 |
| allow_origins_by_regex | array | 可选 | nil | | 使用正则表达式数组来匹配允许跨域访问的 Origin,如[".*\.test.com"] 可以匹配任何test.com的子域名`*`。 |
+| allow_origins_by_metadata | array | 可选 | nil | | 通过引用插件元数据的 `allow_origins` 配置允许跨域访问的 Origin。比如当元数据为 `"allow_origins": {"EXAMPLE": "https://example.com"}` 时,配置 `["EXAMPLE"]` 将允许 Origin `https://example.com` 的访问 |
> **提示**
>
> 请注意 `allow_credential` 是一个很敏感的选项,谨慎选择开启。开启之后,其他参数默认的 `*` 将失效,你必须显式指定它们的值。
> 使用 `**` 时要充分理解它引入了一些安全隐患,比如 CSRF,所以确保这样的安全等级符合自己预期再使用。
+## 元数据
+
+| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |
+| ----------- | ------ | ------ | ----- | ----- | ------------------ |
+| allow_origins | object | 可选 | | | 定义允许跨域访问的 Origin;它的键为 `allow_origins_by_metadata` 使用的引用键, 值则为允许跨域访问的 Origin,其语义与 `allow_origins` 相同 |
+
## 如何启用
创建 `Route` 或 `Service` 对象,并配置 `cors` 插件。
diff --git a/t/plugin/cors3.t b/t/plugin/cors3.t
new file mode 100644
index 0000000..92210a1
--- /dev/null
+++ b/t/plugin/cors3.t
@@ -0,0 +1,353 @@
+#
+# 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();
+log_level("info");
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if (!$block->request) {
+ $block->set_value("request", "GET /t");
+ }
+
+ if (!$block->no_error_log && !$block->error_log) {
+ $block->set_value("no_error_log", "[error]\n[alert]");
+ }
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: validate metadata allow_origins
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.cors")
+ local schema_type = require("apisix.core").schema.TYPE_METADATA
+ local function validate(val)
+ local conf = {}
+ conf.allow_origins = val
+ return plugin.check_schema(conf, schema_type)
+ end
+
+ local good = {
+ key_1 = "*",
+ key_2 = "**",
+ key_3 = "null",
+ key_4 = "http://y.com.uk",
+ key_5 = "https://x.com",
+ key_6 = "https://x.com,http://y.com.uk",
+ key_7 = "https://x.com,http://y.com.uk,http://c.tv",
+ key_8 = "https://x.com,http://y.com.uk:12000,http://c.tv",
+ }
+ local ok, err = validate(good)
+ if not ok then
+ ngx.say("failed to validate ", g, ", ", err)
+ end
+
+ local bad = {
+ "",
+ "*a",
+ "*,http://y.com",
+ "nulll",
+ "http//y.com.uk",
+ "x.com",
+ "https://x.com,y.com.uk",
+ "https://x.com,*,https://y.com.uk",
+ "https://x.com,http://y.com.uk,http:c.tv",
+ }
+ for _, b in ipairs(bad) do
+ local ok, err = validate({key = b})
+ if ok then
+ ngx.say("failed to reject ", b)
+ end
+ end
+
+ ngx.say("done")
+ }
+ }
+--- response_body
+done
+
+
+
+=== TEST 2: set plugin metadata
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/plugin_metadata/cors',
+ ngx.HTTP_PUT,
+ [[{
+ "allow_origins": {
+ "key_1": "https://domain.com",
+ "key_2": "https://sub.domain.com,https://sub2.domain.com",
+ "key_3": "*"
+ },
+ "inactive_timeout": 1
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 3: set route (allow_origins_by_metadata specified)
+--- 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": {
+ "cors": {
+ "allow_origins": "https://test.com",
+ "allow_origins_by_metadata": ["key_1"]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 4: origin not match
+--- request
+GET /hello HTTP/1.1
+--- more_headers
+Origin: http://foo.example.org
+--- response_body
+hello world
+--- response_headers
+Access-Control-Allow-Origin:
+Vary:
+Access-Control-Allow-Methods:
+Access-Control-Allow-Headers:
+Access-Control-Expose-Headers:
+Access-Control-Max-Age:
+Access-Control-Allow-Credentials:
+
+
+
+=== TEST 5: origin matches with allow_origins
+--- request
+GET /hello HTTP/1.1
+--- more_headers
+Origin: https://test.com
+resp-vary: Via
+--- response_body
+hello world
+--- response_headers
+Access-Control-Allow-Origin: https://test.com
+Vary: Via, Origin
+Access-Control-Allow-Methods: *
+Access-Control-Allow-Headers: *
+Access-Control-Expose-Headers: *
+Access-Control-Max-Age: 5
+Access-Control-Allow-Credentials:
+
+
+
+=== TEST 6: origin matches with allow_origins_by_metadata
+--- request
+GET /hello HTTP/1.1
+--- more_headers
+Origin: https://domain.com
+resp-vary: Via
+--- response_body
+hello world
+--- response_headers
+Access-Control-Allow-Origin: https://domain.com
+Vary: Via, Origin
+Access-Control-Allow-Methods: *
+Access-Control-Allow-Headers: *
+Access-Control-Expose-Headers: *
+Access-Control-Max-Age: 5
+Access-Control-Allow-Credentials:
+
+
+
+=== TEST 7: set route (multiple allow_origins_by_metadata specified)
+--- 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": {
+ "cors": {
+ "allow_origins": "https://test.com",
+ "allow_origins_by_metadata": ["key_1", "key_2"]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 8: origin not match
+--- request
+GET /hello HTTP/1.1
+--- more_headers
+Origin: http://foo.example.org
+--- response_body
+hello world
+--- response_headers
+Access-Control-Allow-Origin:
+Vary:
+Access-Control-Allow-Methods:
+Access-Control-Allow-Headers:
+Access-Control-Expose-Headers:
+Access-Control-Max-Age:
+Access-Control-Allow-Credentials:
+
+
+
+=== TEST 9: origin matches with first allow_origins_by_metadata
+--- request
+GET /hello HTTP/1.1
+--- more_headers
+Origin: https://domain.com
+resp-vary: Via
+--- response_body
+hello world
+--- response_headers
+Access-Control-Allow-Origin: https://domain.com
+Vary: Via, Origin
+Access-Control-Allow-Methods: *
+Access-Control-Allow-Headers: *
+Access-Control-Expose-Headers: *
+Access-Control-Max-Age: 5
+Access-Control-Allow-Credentials:
+
+
+
+=== TEST 10: origin matches with second allow_origins_by_metadata
+--- request
+GET /hello HTTP/1.1
+--- more_headers
+Origin: https://sub.domain.com
+resp-vary: Via
+--- response_body
+hello world
+--- response_headers
+Access-Control-Allow-Origin: https://sub.domain.com
+Vary: Via, Origin
+Access-Control-Allow-Methods: *
+Access-Control-Allow-Headers: *
+Access-Control-Expose-Headers: *
+Access-Control-Max-Age: 5
+Access-Control-Allow-Credentials:
+
+
+
+=== TEST 11: set route (wildcard in allow_origins_by_metadata)
+--- 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": {
+ "cors": {
+ "allow_origins": "https://test.com",
+ "allow_origins_by_metadata": ["key_3"]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 12: origin matches by wildcard
+--- request
+GET /hello HTTP/1.1
+--- more_headers
+Origin: http://foo.example.org
+--- response_body
+hello world
+--- response_headers
+Access-Control-Allow-Origin: http://foo.example.org
+Vary: Origin
+Access-Control-Allow-Methods: *
+Access-Control-Allow-Headers: *
+Access-Control-Expose-Headers: *
+Access-Control-Max-Age: 5
+Access-Control-Allow-Credentials: