Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: set cors allow origins by plugin metadata #6546

81 changes: 71 additions & 10 deletions apisix/plugins/cors.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,34 @@
-- 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
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 = {
Expand All @@ -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 = {
Expand Down Expand Up @@ -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,
},
}
}

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -177,13 +208,19 @@ 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_conf, ctx, req_origin,
cache_key, cache_version)
local allow_origins = allow_origins_conf
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to declare allow_origins_conf as local allow_origins here, just change the allow_origins_conf -> allow_origins in parameter list is ok?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, changed that

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)

if not (cache_key and cache_version) then
cache_key, cache_version = core.lrucache.plugin_ctx_id(ctx)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plugin_ctx_id only returns an ID?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😂fixed

end
local multiple_origin, err = lrucache(
cache_key, cache_version, create_multiple_origin_cache, allow_origins_conf
)
if err then
return 500, {message = "get multiple origin cache failed: " .. err}
end
Expand Down Expand Up @@ -225,6 +262,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
Expand All @@ -239,10 +295,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)
Expand Down
7 changes: 7 additions & 0 deletions docs/en/latest/plugins/cors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions docs/zh/latest/plugins/cors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 插件。
Expand Down
Loading