From 75c8ac7a51c72c6e3329b2b03f0532b148968533 Mon Sep 17 00:00:00 2001 From: schnee Date: Thu, 7 Sep 2023 21:16:44 +0800 Subject: [PATCH] feat: add data source plugin default config api (#1225) --- .../apis/web/data_source/serializers.py | 4 + .../bkuser/apis/web/data_source/urls.py | 6 + .../bkuser/apis/web/data_source/views.py | 21 +- .../data_source/plugins/local/constants.py | 2 +- .../apps/data_source/plugins/local/models.py | 4 +- src/bk-user/bkuser/biz/data_source_plugin.py | 200 ++++++++++++++++++ src/bk-user/bkuser/common/error_codes.py | 4 + .../apis/web/data_source/test_data_source.py | 14 +- 8 files changed, 249 insertions(+), 6 deletions(-) create mode 100644 src/bk-user/bkuser/biz/data_source_plugin.py diff --git a/src/bk-user/bkuser/apis/web/data_source/serializers.py b/src/bk-user/bkuser/apis/web/data_source/serializers.py index 84c96dd50..b2a9d19df 100644 --- a/src/bk-user/bkuser/apis/web/data_source/serializers.py +++ b/src/bk-user/bkuser/apis/web/data_source/serializers.py @@ -119,6 +119,10 @@ class DataSourcePluginOutputSLZ(serializers.Serializer): logo = serializers.CharField(help_text="数据源插件 Logo") +class DataSourcePluginDefaultConfigOutputSLZ(serializers.Serializer): + config = serializers.JSONField(help_text="数据源插件默认配置") + + class DataSourceRetrieveOutputSLZ(serializers.Serializer): id = serializers.IntegerField(help_text="数据源 ID") name = serializers.CharField(help_text="数据源名称") diff --git a/src/bk-user/bkuser/apis/web/data_source/urls.py b/src/bk-user/bkuser/apis/web/data_source/urls.py index 879fddb61..01fa2e675 100644 --- a/src/bk-user/bkuser/apis/web/data_source/urls.py +++ b/src/bk-user/bkuser/apis/web/data_source/urls.py @@ -15,6 +15,12 @@ urlpatterns = [ # 数据源插件列表 path("plugins/", views.DataSourcePluginListApi.as_view(), name="data_source_plugin.list"), + # 数据源插件默认配置 + path( + "plugins//default-config/", + views.DataSourcePluginDefaultConfigApi.as_view(), + name="data_source_plugin.default_config", + ), # 数据源创建/获取列表 path("", views.DataSourceListCreateApi.as_view(), name="data_source.list_create"), # 数据源更新/获取 diff --git a/src/bk-user/bkuser/apis/web/data_source/views.py b/src/bk-user/bkuser/apis/web/data_source/views.py index b2582624d..0e4bacdf3 100644 --- a/src/bk-user/bkuser/apis/web/data_source/views.py +++ b/src/bk-user/bkuser/apis/web/data_source/views.py @@ -16,6 +16,7 @@ from bkuser.apis.web.data_source.serializers import ( DataSourceCreateInputSLZ, DataSourceCreateOutputSLZ, + DataSourcePluginDefaultConfigOutputSLZ, DataSourcePluginOutputSLZ, DataSourceRetrieveOutputSLZ, DataSourceSearchInputSLZ, @@ -29,6 +30,7 @@ from bkuser.apps.data_source.models import DataSource, DataSourcePlugin from bkuser.apps.data_source.plugins.constants import DATA_SOURCE_PLUGIN_CONFIG_SCHEMA_MAP from bkuser.apps.data_source.signals import post_create_data_source, post_update_data_source +from bkuser.biz.data_source_plugin import DefaultPluginConfigProvider from bkuser.common.error_codes import error_codes from bkuser.common.views import ExcludePatchAPIViewMixin, ExcludePutAPIViewMixin @@ -39,7 +41,7 @@ class DataSourcePluginListApi(generics.ListAPIView): serializer_class = DataSourcePluginOutputSLZ @swagger_auto_schema( - tags=["data_source"], + tags=["data_source_plugin"], operation_description="数据源插件列表", responses={status.HTTP_200_OK: DataSourcePluginOutputSLZ(many=True)}, ) @@ -47,6 +49,23 @@ def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) +class DataSourcePluginDefaultConfigApi(generics.RetrieveAPIView): + @swagger_auto_schema( + tags=["data_source_plugin"], + operation_description="数据源插件默认配置", + responses={ + status.HTTP_200_OK: DataSourcePluginDefaultConfigOutputSLZ(), + **DATA_SOURCE_PLUGIN_CONFIG_SCHEMA_MAP, + }, + ) + def get(self, request, *args, **kwargs): + config = DefaultPluginConfigProvider().get(kwargs["id"]) + if not config: + raise error_codes.DATA_SOURCE_PLUGIN_NOT_DEFAULT_CONFIG + + return Response(DataSourcePluginDefaultConfigOutputSLZ(instance={"config": config.model_dump()}).data) + + class DataSourceListCreateApi(CurrentUserTenantMixin, generics.ListCreateAPIView): pagination_class = None serializer_class = DataSourceSearchOutputSLZ diff --git a/src/bk-user/bkuser/apps/data_source/plugins/local/constants.py b/src/bk-user/bkuser/apps/data_source/plugins/local/constants.py index 8fcf3a834..23b156114 100644 --- a/src/bk-user/bkuser/apps/data_source/plugins/local/constants.py +++ b/src/bk-user/bkuser/apps/data_source/plugins/local/constants.py @@ -22,7 +22,7 @@ ONE_DAY_SECONDS = 24 * 60 * 60 # 密码可选最长有效期:10年 -MAX_PASSWORD_VALID_TIME = 10 * 365 * ONE_DAY_SECONDS +MAX_PASSWORD_VALID_TIME = 10 * 365 # 可选最长锁定时间:10年 MAX_LOCK_TIME = 10 * 365 * ONE_DAY_SECONDS diff --git a/src/bk-user/bkuser/apps/data_source/plugins/local/models.py b/src/bk-user/bkuser/apps/data_source/plugins/local/models.py index dba53c767..94158f343 100644 --- a/src/bk-user/bkuser/apps/data_source/plugins/local/models.py +++ b/src/bk-user/bkuser/apps/data_source/plugins/local/models.py @@ -57,7 +57,7 @@ class PasswordRuleConfig(BaseModel): # 不允许重复字母,数字,特殊字符 not_repeated_symbol: bool - # 密码有效期(单位:秒) + # 密码有效期(单位:天) valid_time: int = Field(ge=NEVER_EXPIRE_TIME, le=MAX_PASSWORD_VALID_TIME) # 密码试错次数 max_retries: int = Field(ge=0, le=PASSWORD_MAX_RETRIES) @@ -140,7 +140,7 @@ class PasswordInitialConfig(BaseModel): class PasswordExpireConfig(BaseModel): """密码到期相关配置""" - # 在密码到期多久前提醒,单位:秒,多个值表示多次提醒 + # 在密码到期多久前提醒,单位:天,多个值表示多次提醒 remind_before_expire: List[int] # 通知相关配置 notification: NotificationConfig diff --git a/src/bk-user/bkuser/biz/data_source_plugin.py b/src/bk-user/bkuser/biz/data_source_plugin.py new file mode 100644 index 000000000..dceb29a0d --- /dev/null +++ b/src/bk-user/bkuser/biz/data_source_plugin.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available. +Copyright (C) 2017-2021 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. +""" +from typing import Optional + +from pydantic import BaseModel + +from bkuser.apps.data_source.constants import DataSourcePluginEnum +from bkuser.apps.data_source.plugins.local.constants import ( + NotificationMethod, + NotificationScene, + PasswordGenerateMethod, +) +from bkuser.apps.data_source.plugins.local.models import ( + LocalDataSourcePluginConfig, + NotificationConfig, + NotificationTemplate, + PasswordExpireConfig, + PasswordInitialConfig, + PasswordRuleConfig, +) + + +class DefaultPluginConfigProvider: + """默认插件配置提供者""" + + def get(self, plugin_id: str) -> Optional[BaseModel]: + """获取指定插件类型的默认插件配置""" + if plugin_id == DataSourcePluginEnum.LOCAL: + return self._get_default_local_plugin_config() + + return None + + def _get_default_local_plugin_config(self) -> BaseModel: + return LocalDataSourcePluginConfig( + enable_account_password_login=True, + password_rule=PasswordRuleConfig( + min_length=12, + contain_lowercase=True, + contain_uppercase=True, + contain_digit=True, + contain_punctuation=True, + not_continuous_count=0, + not_keyboard_order=False, + not_continuous_letter=False, + not_continuous_digit=False, + not_repeated_symbol=False, + valid_time=30, + max_retries=3, + lock_time=60 * 60, + ), + password_initial=PasswordInitialConfig( + force_change_at_first_login=True, + cannot_use_previous_password=True, + reserved_previous_password_count=3, + generate_method=PasswordGenerateMethod.RANDOM, + fixed_password=None, + notification=NotificationConfig( + enabled_methods=[NotificationMethod.EMAIL], + templates=[ + NotificationTemplate( + method=NotificationMethod.EMAIL, + scene=NotificationScene.USER_INITIALIZE, + title="蓝鲸智云 - 您的账户已经成功创建!", + sender="蓝鲸智云", + content=( + "您好:\n" + + "您的蓝鲸智云帐户已经成功创建,以下是您的帐户信息\n" + + " 登录帐户:{username},初始登录密码:{password}\n" + + "为了保障帐户安全,建议您尽快登录平台修改密码:{url}\n" + + "此邮件为系统自动发送,请勿回复。" + ), + content_html=( + "

您好:

" + + "

您的蓝鲸智云帐户已经成功创建,以下是您的帐户信息

" + + "

登录帐户:{username},初始登录密码:{password}

" + + "

为了保障帐户安全,建议您尽快登录平台修改密码:{url}

" + + "

此邮件为系统自动发送,请勿回复。

" + ), + ), + NotificationTemplate( + method=NotificationMethod.EMAIL, + scene=NotificationScene.RESET_PASSWORD, + title="蓝鲸智云 - 登录密码重置", + sender="蓝鲸智云", + content=( + "您好:\n" + + "我们收到了您重置密码的申请,请点击下方链接进行密码重置:{url}\n" + + "该链接有效时间为 3 小时,过期后请重新点击密码重置链接:{reset_url}\n" + + "此邮件为系统自动发送,请勿回复。" + ), + content_html=( + "

您好:

" + + "

我们收到了您重置密码的申请,请点击下方链接进行密码重置:{url}

" + + "

该链接有效时间为 3 小时,过期后请重新点击密码重置链接:{reset_url}

" + + "

此邮件为系统自动发送,请勿回复。

" + ), + ), + NotificationTemplate( + method=NotificationMethod.SMS, + scene=NotificationScene.USER_INITIALIZE, + title=None, + sender="蓝鲸智云", + content=( + "您好:\n" + + "您的蓝鲸智云帐户已经成功创建,以下是您的帐户信息\n" + + " 登录帐户:{username},初始登录密码:{password}\n" + + "为了保障帐户安全,建议您尽快登录平台修改密码:{url}\n" + + "此邮件为系统自动发送,请勿回复。" + ), + content_html=None, + ), + NotificationTemplate( + method=NotificationMethod.SMS, + scene=NotificationScene.RESET_PASSWORD, + title=None, + sender="蓝鲸智云", + content=( + "您好:\n" + + "我们收到了您重置密码的申请,请点击下方链接进行密码重置:{url}\n" + + "该链接有效时间为 3 小时,过期后请重新点击密码重置链接:{reset_url}\n" + + "此邮件为系统自动发送,请勿回复。" + ), + content_html=None, + ), + ], + ), + ), + password_expire=PasswordExpireConfig( + remind_before_expire=[1, 7, 15], + notification=NotificationConfig( + enabled_methods=[NotificationMethod.EMAIL], + templates=[ + NotificationTemplate( + method=NotificationMethod.EMAIL, + scene=NotificationScene.PASSWORD_EXPIRING, + title="蓝鲸智云 - 密码即将到期提醒", + sender="蓝鲸智云", + content=( + "{username},您好:\n" + + "您的蓝鲸智云平台密码将于 {expired_at} 天后过期,为避免影响使用,请尽快登陆平台修改密码。\n" # noqa: E501 + + "此邮件为系统自动发送,请勿回复。" + ), + content_html=( + "

{username},您好:

" + + "

您的蓝鲸智云平台密码将于 {expired_at} 天后过期,为避免影响使用,请尽快登陆平台修改密码。

" # noqa: E501 + + "

此邮件为系统自动发送,请勿回复。

" + ), + ), + NotificationTemplate( + method=NotificationMethod.EMAIL, + scene=NotificationScene.PASSWORD_EXPIRED, + title="蓝鲸智云 - 密码已过期提醒", + sender="蓝鲸智云", + content=( + "{username},您好:\n" + + "您的蓝鲸智云平台密码已过期,为避免影响正常使用,请尽快登陆平台修改密码。\n" # noqa: E501 + + "此邮件为系统自动发送,请勿回复。" + ), + content_html=( + "

{username},您好:

" + + "

您的蓝鲸智云平台密码已过期,为避免影响正常使用,请尽快登陆平台修改密码。

" + + "

此邮件为系统自动发送,请勿回复。

" + ), + ), + NotificationTemplate( + method=NotificationMethod.SMS, + scene=NotificationScene.PASSWORD_EXPIRING, + title=None, + sender="蓝鲸智云", + content=( + "{username},您好:\n" + + "您的蓝鲸智云平台密码将于 {expired_at} 天后过期,为避免影响使用,请尽快登陆平台修改密码。\n" # noqa: E501 + + "此邮件为系统自动发送,请勿回复。" + ), + content_html=None, + ), + NotificationTemplate( + method=NotificationMethod.SMS, + scene=NotificationScene.PASSWORD_EXPIRED, + title=None, + sender="蓝鲸智云", + content=( + "{username},您好:\n" + + "您的蓝鲸智云平台密码已过期,为避免影响使用,请尽快登陆平台修改密码。\n" # noqa: E501 + + "此邮件为系统自动发送,请勿回复。" + ), + content_html=None, + ), + ], + ), + ), + ) diff --git a/src/bk-user/bkuser/common/error_codes.py b/src/bk-user/bkuser/common/error_codes.py index 5ff9b61c5..553712b8e 100644 --- a/src/bk-user/bkuser/common/error_codes.py +++ b/src/bk-user/bkuser/common/error_codes.py @@ -71,6 +71,10 @@ class ErrorCodes: # 调用外部系统API REMOTE_REQUEST_ERROR = ErrorCode(_("调用外部系统API异常")) + + # 数据源插件 + DATA_SOURCE_PLUGIN_NOT_DEFAULT_CONFIG = ErrorCode(_("当前数据源插件未提供默认配置")) + # 数据源 DATA_SOURCE_OPERATION_UNSUPPORTED = ErrorCode(_("数据源不支持该操作")) DATA_SOURCE_NOT_EXIST = ErrorCode(_("数据源不存在")) diff --git a/src/bk-user/tests/apis/web/data_source/test_data_source.py b/src/bk-user/tests/apis/web/data_source/test_data_source.py index 8cce58e12..854b882e2 100644 --- a/src/bk-user/tests/apis/web/data_source/test_data_source.py +++ b/src/bk-user/tests/apis/web/data_source/test_data_source.py @@ -42,7 +42,7 @@ def local_ds_plugin_config() -> Dict[str, Any]: "not_continuous_letter": True, "not_continuous_digit": True, "not_repeated_symbol": True, - "valid_time": 86400, + "valid_time": 7, "max_retries": 3, "lock_time": 3600, }, @@ -87,7 +87,7 @@ def local_ds_plugin_config() -> Dict[str, Any]: }, }, "password_expire": { - "remind_before_expire": [3600, 7200], + "remind_before_expire": [1, 7], "notification": { "enabled_methods": [NotificationMethod.EMAIL, NotificationMethod.SMS], "templates": [ @@ -153,6 +153,16 @@ def test_list(self, api_client): assert DataSourcePluginEnum.LOCAL in [d["id"] for d in resp.data] +class TestDataSourcePluginDefaultConfigApi: + def test_retrieve(self, api_client): + resp = api_client.get(reverse("data_source_plugin.default_config", args=[DataSourcePluginEnum.LOCAL.value])) + assert resp.status_code == status.HTTP_200_OK + + def test_retrieve_not_exists(self, api_client): + resp = api_client.get(reverse("data_source_plugin.default_config", args=["not_exists"])) + assert resp.status_code == status.HTTP_400_BAD_REQUEST + + class TestDataSourceCreateApi: def test_create_local_data_source(self, api_client, local_ds_plugin_config): resp = api_client.post(