From 6e6adbab95514cbe562bb4590d81cb82787925e2 Mon Sep 17 00:00:00 2001 From: alex-smile <443677891@qq.com> Date: Sun, 25 Jun 2023 10:36:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20cors=20form=20=E7=9A=84?= =?UTF-8?q?=E6=AD=A3=E5=88=99=E6=A0=A1=E9=AA=8C;=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=20API=20=E6=96=87=E6=A1=A3=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=20(#58)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 更新 cors form 的正则校验; 修复组件 API 文档菜单展示 * 修复 feature 单元测试 * 添加 license --- src/dashboard-front/src/App.vue | 2 +- .../apigateway/apps/feature/views.py | 1 + .../migrations/0006_auto_20230620_1512.py | 45 +++++ .../apigateway/apps/plugin/models.py | 2 +- .../apigateway/apps/plugin/plugin/checker.py | 43 ++++- .../apigateway/fixtures/plugins.yaml | 23 ++- .../locale/en/LC_MESSAGES/django.mo | Bin 37923 -> 37936 bytes .../locale/en/LC_MESSAGES/django.po | 25 +-- .../tests/apps/feature/test_views.py | 3 +- .../tests/apps/plugin/plugin/test_checker.py | 86 ++++++++++ .../apigateway/tests/fixures/__init__.py | 17 ++ .../tests/fixures/test_plugins_yaml.py | 157 ++++++++++++++++++ 12 files changed, 378 insertions(+), 26 deletions(-) create mode 100644 src/dashboard/apigateway/apigateway/apps/plugin/migrations/0006_auto_20230620_1512.py create mode 100644 src/dashboard/apigateway/apigateway/tests/fixures/__init__.py create mode 100644 src/dashboard/apigateway/apigateway/tests/fixures/test_plugins_yaml.py diff --git a/src/dashboard-front/src/App.vue b/src/dashboard-front/src/App.vue index 0ad0f3763..622d40270 100644 --- a/src/dashboard-front/src/App.vue +++ b/src/dashboard-front/src/App.vue @@ -483,7 +483,7 @@ name: i18n.t('组件API文档'), id: 5, url: 'componentAPI', - enabled: this.GLOBAL_CONFIG.PLATFORM_FEATURE.MENU_ITEM_ESB_API + enabled: this.GLOBAL_CONFIG.PLATFORM_FEATURE.MENU_ITEM_ESB_API_DOC }, { name: this.$t('网关API SDK'), diff --git a/src/dashboard/apigateway/apigateway/apps/feature/views.py b/src/dashboard/apigateway/apigateway/apps/feature/views.py index f42262b2c..c21f49da2 100644 --- a/src/dashboard/apigateway/apigateway/apps/feature/views.py +++ b/src/dashboard/apigateway/apigateway/apps/feature/views.py @@ -37,6 +37,7 @@ def list(self, request, *args, **kwargs): feature_flags.update( { "MENU_ITEM_ESB_API": feature_flags.get("MENU_ITEM_ESB_API", False) and request.user.is_superuser, + "MENU_ITEM_ESB_API_DOC": feature_flags.get("MENU_ITEM_ESB_API", False), } ) diff --git a/src/dashboard/apigateway/apigateway/apps/plugin/migrations/0006_auto_20230620_1512.py b/src/dashboard/apigateway/apigateway/apps/plugin/migrations/0006_auto_20230620_1512.py new file mode 100644 index 000000000..cc1166864 --- /dev/null +++ b/src/dashboard/apigateway/apigateway/apps/plugin/migrations/0006_auto_20230620_1512.py @@ -0,0 +1,45 @@ +# +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the License at +# +# http://opensource.org/licenses/MIT +# +# 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. +# +# Generated by Django 3.2.18 on 2023-06-20 07:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plugin', '0005_auto_20230227_2006'), + ] + + operations = [ + migrations.AlterField( + model_name='pluginbinding', + name='scope_type', + field=models.CharField(choices=[('stage', '环境'), ('resource', '资源')], db_index=True, max_length=32), + ), + migrations.AlterField( + model_name='pluginconfig', + name='description', + field=models.TextField(blank=True, default=None, null=True), + ), + migrations.AlterField( + model_name='pluginconfig', + name='description_en', + field=models.TextField(blank=True, default=None, null=True), + ), + ] diff --git a/src/dashboard/apigateway/apigateway/apps/plugin/models.py b/src/dashboard/apigateway/apigateway/apps/plugin/models.py index c4a059fd1..adf548522 100644 --- a/src/dashboard/apigateway/apigateway/apps/plugin/models.py +++ b/src/dashboard/apigateway/apigateway/apps/plugin/models.py @@ -130,7 +130,7 @@ class PluginConfig(OperatorModelMixin, TimestampedModelMixin): api = models.ForeignKey(Gateway, on_delete=models.CASCADE) name = models.CharField(max_length=64, db_index=True) type = models.ForeignKey(PluginType, null=True, on_delete=models.PROTECT) - description_i18n = I18nProperty(models.TextField(blank=True, default="")) + description_i18n = I18nProperty(models.TextField(default=None, blank=True, null=True)) description = description_i18n.default_field() description_en = description_i18n.field("en") yaml = models.TextField(blank=True, default=None, null=True) diff --git a/src/dashboard/apigateway/apigateway/apps/plugin/plugin/checker.py b/src/dashboard/apigateway/apigateway/apps/plugin/plugin/checker.py index ad267c76e..56ac7a0c3 100644 --- a/src/dashboard/apigateway/apigateway/apps/plugin/plugin/checker.py +++ b/src/dashboard/apigateway/apigateway/apps/plugin/plugin/checker.py @@ -23,7 +23,8 @@ - apisix 插件的 check_schema 除校验 schema 外,可能还有一些额外的校验,这些插件配置的额外校验,放在此模块处理 """ import re -from typing import ClassVar, Dict +from collections import Counter +from typing import ClassVar, Dict, List, Optional from django.utils.translation import gettext as _ @@ -40,22 +41,48 @@ class BkCorsChecker(BaseChecker): def check(self, yaml_: str): loaded_data = yaml_loads(yaml_) + self._check_allow_origins(loaded_data.get("allow_origins")) + self._check_allow_origins_by_regex(loaded_data.get("allow_origins_by_regex")) + self._check_allow_methods(loaded_data["allow_methods"]) + self._check_headers(loaded_data["allow_headers"], key="allow_headers") + self._check_headers(loaded_data["expose_headers"], key="expose_headers") + if loaded_data.get("allow_credential"): for key in ["allow_origins", "allow_methods", "allow_headers", "expose_headers"]: if loaded_data.get(key) == "*": raise ValueError(_("当 'allow_credential' 为 True 时, {key} 不能为 '*'。").format(key=key)) - if loaded_data.get("allow_origins_by_regex"): - for re_rule in loaded_data["allow_origins_by_regex"]: - try: - re.compile(re_rule) - except Exception: - raise ValueError(_("allow_origins_by_regex 中数据 '{re_rule}' 不是合法的正则表达式。").format(re_rule=re_rule)) - # 非 apisix check_schema 中逻辑,根据业务需要添加的校验逻辑 if not (loaded_data.get("allow_origins") or loaded_data.get("allow_origins_by_regex")): raise ValueError(_("allow_origins, allow_origins_by_regex 不能同时为空。")) + def _check_allow_origins(self, allow_origins: Optional[str]): + if not allow_origins: + return + self._check_duplicate_items(allow_origins.split(","), "allow_origins") + + def _check_allow_methods(self, allow_methods: str): + self._check_duplicate_items(allow_methods.split(","), "allow_methods") + + def _check_headers(self, headers: str, key: str): + self._check_duplicate_items(headers.split(","), key) + + def _check_allow_origins_by_regex(self, allow_origins_by_regex: Optional[str]): + if not allow_origins_by_regex: + return + + # 必须是一个合法的正则表达式 + for re_rule in allow_origins_by_regex: + try: + re.compile(re_rule) + except Exception: + raise ValueError(_("allow_origins_by_regex 中数据 '{re_rule}' 不是合法的正则表达式。").format(re_rule=re_rule)) + + def _check_duplicate_items(self, data: List[str], key: str): + duplicate_items = [item for item, count in Counter(data).items() if count >= 2] + if duplicate_items: + raise ValueError(_("{} 存在重复的元素:{}。").format(key, ", ".join(duplicate_items))) + class PluginConfigYamlChecker: type_code_to_checker: ClassVar[Dict[str, BaseChecker]] = { diff --git a/src/dashboard/apigateway/apigateway/fixtures/plugins.yaml b/src/dashboard/apigateway/apigateway/fixtures/plugins.yaml index 6e438f4bc..0d83c09af 100644 --- a/src/dashboard/apigateway/apigateway/fixtures/plugins.yaml +++ b/src/dashboard/apigateway/apigateway/fixtures/plugins.yaml @@ -124,7 +124,8 @@ "allow_origins": { "description": "允许跨域访问的 Origin,格式为 scheme://host:port,示例如 https://example.com:8081。如果你有多个 Origin,请使用 , 分隔。当 allow_credential 为 false 时,可以使用 * 来表示允许所有 Origin 通过。你也可以在启用了 allow_credential 后使用 ** 强制允许所有 Origin 均通过,但请注意这样存在安全隐患。", "type": "string", - "pattern": "^(|\\*|\\*\\*|null|\\w+://[^,]+(,\\w+://[^,]+)*)$", + "pattern": "^(|\\*|\\*\\*|null|http(s)?://[-a-zA-Z0-9:\\[\\]\\.]+(,http(s)?://[-a-zA-Z0-9:\\[\\]\\.]+)*)$", + "maxLength": 4096, "default": "" }, "allow_origins_by_regex": { @@ -134,7 +135,8 @@ "items": { "type": "string", "minLength": 1, - "maxLength": 4096 + "maxLength": 4096, + "pattern": "^(\\^)?[-a-zA-Z0-9:/\\[\\]\\{\\}\\(\\)\\.\\*\\+\\?\\|\\\\]+(\\$)?$" }, "ui:component": { "name": "bfArray" @@ -144,6 +146,8 @@ "description": "允许跨域访问的 Method,比如:GET,POST 等。如果你有多个 Method,请使用 , 分割。当 allow_credential 为 false 时,可以使用 * 来表示允许所有 Method 通过。你也可以在启用了 allow_credential 后使用 ** 强制允许所有 Method 都通过,但请注意这样存在安全隐患。", "type": "string", "default": "**", + "pattern": "^(\\*|\\*\\*|(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)(,(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE))*)$", + "maxLength": 100, "ui:rules": [ "required" ] @@ -152,6 +156,8 @@ "description": "允许跨域访问时请求方携带哪些非 CORS 规范 以外的 Header。如果你有多个 Header,请使用 , 分割。当 allow_credential 为 false 时,可以使用 * 来表示允许所有 Header 通过。你也可以在启用了 allow_credential 后使用 ** 强制允许所有 Header 都通过,但请注意这样存在安全隐患。", "type": "string", "default": "**", + "pattern": "^(\\*|\\*\\*|[-a-zA-Z0-9]+(,[-a-zA-Z0-9]+)*)$", + "maxLength": 4096, "ui:rules": [ "required" ] @@ -159,6 +165,8 @@ "expose_headers": { "description": "允许跨域访问时响应方携带哪些非 CORS 规范 以外的 Header。如果你有多个 Header,请使用 , 分割。当 allow_credential 为 false 时,可以使用 * 来表示允许任意 Header 。你也可以在启用了 allow_credential 后使用 ** 强制允许任意 Header,但请注意这样存在安全隐患。", "type": "string", + "pattern": "^(|\\*|\\*\\*|[-a-zA-Z0-9]+(,[-a-zA-Z0-9]+)*)$", + "maxLength": 4096, "default": "" }, "max_age": { @@ -208,7 +216,7 @@ "allow_origins": { "description": "Origins to allow CORS. Use the scheme://host:port format. For example, https://example.com:8081. If you have multiple origins, use a , to list them. If allow_credential is set to false, you can enable CORS for all origins by using *. If allow_credential is set to true, you can forcefully allow CORS on all origins by using ** but it will pose some security issues.", "type": "string", - "pattern": "^(|\\*|\\*\\*|null|\\w+://[^,]+(,\\w+://[^,]+)*)$", + "pattern": "^(|\\*|\\*\\*|null|http(s)?://[-a-zA-Z0-9:\\[\\]\\.]+(,http(s)?://[-a-zA-Z0-9:\\[\\]\\.]+)*)$", "default": "" }, "allow_origins_by_regex": { @@ -218,7 +226,8 @@ "items": { "type": "string", "minLength": 1, - "maxLength": 4096 + "maxLength": 4096, + "pattern": "^(\\^)?[-a-zA-Z0-9:/\\[\\]\\{\\}\\(\\)\\.\\*\\+\\?\\|\\\\]+(\\$)?$" }, "ui:component": { "name": "bfArray" @@ -228,6 +237,8 @@ "description": "Request methods to enable CORS on. For example GET, POST. Use , to add multiple methods. If allow_credential is set to false, you can enable CORS for all methods by using *. If allow_credential is set to true, you can forcefully allow CORS on all methods by using ** but it will pose some security issues.", "type": "string", "default": "**", + "pattern": "^(\\*|\\*\\*|(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)(,(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE))*)$", + "maxLength": 100, "ui:rules": [ "required" ] @@ -236,6 +247,8 @@ "description": "Headers in the request allowed when accessing a cross-origin resource. Use , to add multiple headers. If allow_credential is set to false, you can enable CORS for all request headers by using *. If allow_credential is set to true, you can forcefully allow CORS on all request headers by using ** but it will pose some security issues.", "type": "string", "default": "**", + "pattern": "^(\\*|\\*\\*|[-a-zA-Z0-9]+(,[-a-zA-Z0-9]+)*)$", + "maxLength": 4096, "ui:rules": [ "required" ] @@ -243,6 +256,8 @@ "expose_headers": { "description": "Headers in the response allowed when accessing a cross-origin resource. Use , to add multiple headers. If allow_credential is set to false, you can enable CORS for all response headers by using *. If allow_credential is set to true, you can forcefully allow CORS on all response headers by using ** but it will pose some security issues.", "type": "string", + "pattern": "^(|\\*|\\*\\*|[-a-zA-Z0-9]+(,[-a-zA-Z0-9]+)*)$", + "maxLength": 4096, "default": "" }, "max_age": { diff --git a/src/dashboard/apigateway/apigateway/locale/en/LC_MESSAGES/django.mo b/src/dashboard/apigateway/apigateway/locale/en/LC_MESSAGES/django.mo index 6f168c1234a15605a4b4284d909d2985ce8f6ae5..ef4c9fd1d4f77496a8d5730afcf0138e1c188eab 100644 GIT binary patch delta 8267 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\n" "Language-Team: LANGUAGE \n" @@ -1026,21 +1026,25 @@ msgstr "Plugin" msgid "插件绑定" msgstr "Plugin Binding" -#: apigateway/apps/plugin/plugin/checker.py:46 +#: apigateway/apps/plugin/plugin/checker.py:53 #, python-brace-format msgid "当 'allow_credential' 为 True 时, {key} 不能为 '*'。" msgstr "{key} cannot be '*' when 'allow_credential' is True." -#: apigateway/apps/plugin/plugin/checker.py:53 +#: apigateway/apps/plugin/plugin/checker.py:57 +msgid "allow_origins, allow_origins_by_regex 不能同时为空。" +msgstr "" +"allow_origins, allow_origins_by_regex cannot be empty at the same time." + +#: apigateway/apps/plugin/plugin/checker.py:79 #, python-brace-format msgid "allow_origins_by_regex 中数据 '{re_rule}' 不是合法的正则表达式。" msgstr "" "The '{re_rule}' in allow_origins_by_regex is not a legal regex expression." -#: apigateway/apps/plugin/plugin/checker.py:57 -msgid "allow_origins, allow_origins_by_regex 不能同时为空。" -msgstr "" -"allow_origins, allow_origins_by_regex cannot be empty at the same time." +#: apigateway/apps/plugin/plugin/checker.py:84 +msgid "{} 存在重复的元素:{}。" +msgstr "Duplicate element in {}: {}." #: apigateway/apps/plugin/plugin/convertor.py:71 #, python-brace-format @@ -1102,10 +1106,6 @@ msgstr "The shared micro-gateway instance does not exist." msgid "发布中" msgstr "Releasing" -#: apigateway/apps/release/releasers.py:306 -msgid "配置下发成功" -msgstr "configuration release success" - #: apigateway/apps/release/views.py:51 msgid "当前选择环境未发布版本,请先发布版本到该环境。" msgstr "The stage has not released, please release first." @@ -1920,6 +1920,9 @@ msgstr "App [{value}] does not match the required pattern." msgid "蓝鲸应用【{app_codes}】不匹配要求的模式。" msgstr "App [{app_codes}] does not match the required pattern." +#~ msgid "配置下发成功" +#~ msgstr "configuration release success" + #~ msgid "资源名称【name={name}】重复。" #~ msgstr "Resource name [{name}] already exists." diff --git a/src/dashboard/apigateway/apigateway/tests/apps/feature/test_views.py b/src/dashboard/apigateway/apigateway/tests/apps/feature/test_views.py index 093e30d37..d8443fe7b 100644 --- a/src/dashboard/apigateway/apigateway/tests/apps/feature/test_views.py +++ b/src/dashboard/apigateway/apigateway/tests/apps/feature/test_views.py @@ -43,6 +43,7 @@ def test_list(self, settings, request_factory, mocker, faker, is_superuser, expe view = FeatureFlagViewSet.as_view({"get": "list"}) response = view(request) result = get_response_json(response) - assert len(result["data"]) == 2 + assert len(result["data"]) == 3 assert settings.DEFAULT_FEATURE_FLAG == {"MENU_ITEM_ESB_API": True} assert result["data"]["MENU_ITEM_ESB_API"] == expected + assert result["data"]["MENU_ITEM_ESB_API_DOC"] is True diff --git a/src/dashboard/apigateway/apigateway/tests/apps/plugin/plugin/test_checker.py b/src/dashboard/apigateway/apigateway/tests/apps/plugin/plugin/test_checker.py index 78ddbd594..fd52f0bb9 100644 --- a/src/dashboard/apigateway/apigateway/tests/apps/plugin/plugin/test_checker.py +++ b/src/dashboard/apigateway/apigateway/tests/apps/plugin/plugin/test_checker.py @@ -78,6 +78,22 @@ def test_check(self, data): "max_age": 100, "allow_credential": False, }, + { + "allow_origins": "http://foo.com", + "allow_methods": "GET,POST,PUT,GET", + "allow_headers": "**", + "expose_headers": "**", + "max_age": 100, + "allow_credential": False, + }, + { + "allow_origins": "http://foo.com", + "allow_methods": "**", + "allow_headers": "x-token,x-token", + "expose_headers": "", + "max_age": 100, + "allow_credential": False, + }, ], ) def test_check__error(self, data): @@ -85,6 +101,76 @@ def test_check__error(self, data): with pytest.raises(ValueError): checker.check(yaml_dumps(data)) + @pytest.mark.parametrize( + "allow_methods", + [ + "*", + "**", + "GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS,CONNECT,TRACE", + "GET,POST,OPTIONS", + "GET", + ], + ) + def test_check_allow_methods(self, allow_methods): + checker = BkCorsChecker() + assert checker._check_allow_methods(allow_methods) is None + + @pytest.mark.parametrize( + "allow_methods", + [ + "GET,POST,GET", + ], + ) + def test_check_allow_methods__error(self, allow_methods): + checker = BkCorsChecker() + with pytest.raises(ValueError): + checker._check_allow_methods(allow_methods) + + @pytest.mark.parametrize( + "headers", + [ + "Bk-Token", + "Bk-Token,X-Token", + "BK-TOKEN", + ], + ) + def test_check_headers(self, headers): + checker = BkCorsChecker() + assert checker._check_headers(headers, "key") is None + + @pytest.mark.parametrize( + "headers", + [ + "Bk-Token,Bk-Token", + ], + ) + def test_check_headers__error(self, headers): + checker = BkCorsChecker() + with pytest.raises(ValueError): + checker._check_headers(headers, "key") + + @pytest.mark.parametrize( + "data", + [ + ["a", "b"], + ], + ) + def test_check_duplicate_items(self, data): + checker = BkCorsChecker() + result = checker._check_duplicate_items(data, "key") + assert result is None + + @pytest.mark.parametrize( + "data", + [ + ["a", "b", "a"], + ], + ) + def test_check_duplicate_items__error(self, data): + checker = BkCorsChecker() + with pytest.raises(ValueError): + checker._check_duplicate_items(data, "key") + class TestPluginConfigYamlChecker: @pytest.mark.parametrize( diff --git a/src/dashboard/apigateway/apigateway/tests/fixures/__init__.py b/src/dashboard/apigateway/apigateway/tests/fixures/__init__.py new file mode 100644 index 000000000..2941673fe --- /dev/null +++ b/src/dashboard/apigateway/apigateway/tests/fixures/__init__.py @@ -0,0 +1,17 @@ +# +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the License at +# +# http://opensource.org/licenses/MIT +# +# 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. +# diff --git a/src/dashboard/apigateway/apigateway/tests/fixures/test_plugins_yaml.py b/src/dashboard/apigateway/apigateway/tests/fixures/test_plugins_yaml.py new file mode 100644 index 000000000..22ec5c032 --- /dev/null +++ b/src/dashboard/apigateway/apigateway/tests/fixures/test_plugins_yaml.py @@ -0,0 +1,157 @@ +# +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available. +# Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. +# Licensed under the MIT License (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the License at +# +# http://opensource.org/licenses/MIT +# +# 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. +# +import re + +import pytest + + +class TestBkCorsPluginForm: + allow_origins_pattern = ( + "^(|\\*|\\*\\*|null|http(s)?://[-a-zA-Z0-9:\\[\\]\\.]+(,http(s)?://[-a-zA-Z0-9:\\[\\]\\.]+)*)$" + ) + + allow_origins_by_regex_pattern = "^(\\^)?[-a-zA-Z0-9:/\\[\\]\\{\\}\\(\\)\\.\\*\\+\\?\\|\\\\]+(\\$)?$" + + allow_methods_pattern = ( + "^(\\*|\\*\\*|(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)" + "(,(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE))*)$" + ) + + allow_headers_pattern = "^(\\*|\\*\\*|[-a-zA-Z0-9]+(,[-a-zA-Z0-9]+)*)$" + + expose_headers_pattern = "^(|\\*|\\*\\*|[-a-zA-Z0-9]+(,[-a-zA-Z0-9]+)*)$" + + @pytest.mark.parametrize( + "allow_origins", + [ + "", + "*", + "**", + "null", + "http://example.com", + "https://example.com", + "http://example.com:8080", + "http://example.com:8080,http://foo.com:8000", + "http://[1:1::1]:8080", + ], + ) + def test_allow_origins_pattern(self, allow_origins): + assert re.match(self.allow_origins_pattern, allow_origins) + + @pytest.mark.parametrize( + "allow_origins", + [ + "test", + "http://foo.com/", + "http://foo.com:8080,", + "http://foo.com,test", + "http://foo.com,", + "http://foo.com, http://example.com", + ], + ) + def test_allow_origins_pattern__error(self, allow_origins): + assert not re.match(self.allow_origins_pattern, allow_origins) + + @pytest.mark.parametrize( + "allow_origins_by_regex", + [ + "http://foo.com", + "http://.*.foo.com", + "http://foo.com:8080", + "^http://.*\\.foo\\.com:8000$", + "^https://.*\\.foo\\.com$", + "http://[1:1::1]:8000", + "^http(s)?://.*\\.example\\.com$", + "^http(s)?://.*\\.(foo|example)\\.com$", + "http://.+\\.foo\\.com", + ], + ) + def test_allow_origins_by_regex(self, allow_origins_by_regex): + assert re.match(self.allow_origins_by_regex_pattern, allow_origins_by_regex) + + @pytest.mark.parametrize( + "allow_origins_by_regex", + [ + "", + " ", + "http://foo.com,", + ], + ) + def test_allow_origins_by_regex__error(self, allow_origins_by_regex): + assert not re.match(self.allow_origins_by_regex_pattern, allow_origins_by_regex) + + @pytest.mark.parametrize( + "allow_methods", + [ + "*", + "**", + "GET", + "GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS,CONNECT,TRACE", + "GET,POST", + ], + ) + def test_allow_methods(self, allow_methods): + assert re.match(self.allow_methods_pattern, allow_methods) + + @pytest.mark.parametrize( + "allow_methods", + [ + "", + "GET,", + "GET,POST,", + "GET, POST", + ], + ) + def test_allow_methods__error(self, allow_methods): + assert not re.match(self.allow_methods_pattern, allow_methods) + + @pytest.mark.parametrize( + "allow_headers", + [ + "*", + "**", + "Bk-Token", + "Bk-Token,Bk-User", + ], + ) + def test_allow_headers(self, allow_headers): + assert re.match(self.allow_headers_pattern, allow_headers) + + @pytest.mark.parametrize( + "allow_headers", + [ + "", + "Bk-Token, Bk-User", + "Bk_Token", + ], + ) + def test_allow_headers__error(self, allow_headers): + assert not re.match(self.allow_headers_pattern, allow_headers) + + @pytest.mark.parametrize( + "expose_headers", + [ + "", + "*", + "**", + "Bk-Token", + "Bk-Token,Bk-User", + ], + ) + def test_expose_headers(self, expose_headers): + assert re.match(self.expose_headers_pattern, expose_headers)