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: