Skip to content

Commit

Permalink
更新 cors form 的正则校验; 修复组件 API 文档菜单展示 (#58)
Browse files Browse the repository at this point in the history
* 更新 cors form 的正则校验; 修复组件 API 文档菜单展示
* 修复 feature 单元测试
* 添加 license
  • Loading branch information
alex-smile authored Jun 25, 2023
1 parent 795d4d2 commit 6e6adba
Show file tree
Hide file tree
Showing 12 changed files with 378 additions and 26 deletions.
2 changes: 1 addition & 1 deletion src/dashboard-front/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
1 change: 1 addition & 0 deletions src/dashboard/apigateway/apigateway/apps/feature/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
)

Expand Down
Original file line number Diff line number Diff line change
@@ -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),
),
]
2 changes: 1 addition & 1 deletion src/dashboard/apigateway/apigateway/apps/plugin/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
43 changes: 35 additions & 8 deletions src/dashboard/apigateway/apigateway/apps/plugin/plugin/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 _

Expand All @@ -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]] = {
Expand Down
23 changes: 19 additions & 4 deletions src/dashboard/apigateway/apigateway/fixtures/plugins.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -134,7 +135,8 @@
"items": {
"type": "string",
"minLength": 1,
"maxLength": 4096
"maxLength": 4096,
"pattern": "^(\\^)?[-a-zA-Z0-9:/\\[\\]\\{\\}\\(\\)\\.\\*\\+\\?\\|\\\\]+(\\$)?$"
},
"ui:component": {
"name": "bfArray"
Expand All @@ -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"
]
Expand All @@ -152,13 +156,17 @@
"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"
]
},
"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": {
Expand Down Expand Up @@ -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": {
Expand All @@ -218,7 +226,8 @@
"items": {
"type": "string",
"minLength": 1,
"maxLength": 4096
"maxLength": 4096,
"pattern": "^(\\^)?[-a-zA-Z0-9:/\\[\\]\\{\\}\\(\\)\\.\\*\\+\\?\\|\\\\]+(\\$)?$"
},
"ui:component": {
"name": "bfArray"
Expand All @@ -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"
]
Expand All @@ -236,13 +247,17 @@
"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"
]
},
"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": {
Expand Down
Binary file not shown.
25 changes: 14 additions & 11 deletions src/dashboard/apigateway/apigateway/locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-06-16 15:48+0800\n"
"POT-Creation-Date: 2023-06-21 15:03+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."
Expand Down Expand Up @@ -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."

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit 6e6adba

Please sign in to comment.