From b0d2f13f609aa0eef83bb38a618970cfbd858ae3 Mon Sep 17 00:00:00 2001 From: schnee Date: Tue, 14 Nov 2023 10:05:25 +0800 Subject: [PATCH] minor: Init default tenant in migrations (#1388) --- .github/workflows/bk-user.yml | 2 + .../apis/web/data_source/serializers.py | 7 +- .../bkuser/apis/web/data_source/views.py | 8 +- .../bkuser/apis/web/tenant/serializers.py | 6 +- .../migrations/0004_init_default_tenant.py | 78 ++++++ src/bk-user/bkuser/biz/data_source.py | 4 +- src/bk-user/bkuser/biz/data_source_plugin.py | 241 ------------------ src/bk-user/bkuser/common/hashers/pbkdf2.py | 2 +- src/bk-user/bkuser/common/passwd/generator.py | 3 +- .../bkuser/monitoring/healthz/probes.py | 27 ++ src/bk-user/bkuser/plugins/README.md | 3 +- src/bk-user/bkuser/plugins/base.py | 13 +- .../bkuser/plugins/general/__init__.py | 3 +- .../bkuser/plugins/general/defaults.py | 27 ++ src/bk-user/bkuser/plugins/local/__init__.py | 3 +- src/bk-user/bkuser/plugins/local/defaults.py | 203 +++++++++++++++ src/bk-user/bkuser/settings.py | 1 + .../apis/web/data_source/test_data_source.py | 2 +- src/bk-user/tests/test_utils/tenant.py | 6 +- 19 files changed, 373 insertions(+), 266 deletions(-) create mode 100644 src/bk-user/bkuser/apps/tenant/migrations/0004_init_default_tenant.py delete mode 100644 src/bk-user/bkuser/biz/data_source_plugin.py create mode 100644 src/bk-user/bkuser/plugins/general/defaults.py create mode 100644 src/bk-user/bkuser/plugins/local/defaults.py diff --git a/.github/workflows/bk-user.yml b/.github/workflows/bk-user.yml index a0eae0f07..cac608199 100644 --- a/.github/workflows/bk-user.yml +++ b/.github/workflows/bk-user.yml @@ -75,5 +75,7 @@ jobs: export BK_COMPONENT_API_URL="" export MYSQL_PASSWORD=root_pwd export MYSQL_HOST="127.0.0.1" + export INITIAL_ADMIN_USERNAME=admin + export INITIAL_ADMIN_PASSWORD=admin_pwd export DJANGO_SETTINGS_MODULE=bkuser.settings poetry run pytest ./tests 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 a075a2fef..a1eb52b53 100644 --- a/src/bk-user/bkuser/apis/web/data_source/serializers.py +++ b/src/bk-user/bkuser/apis/web/data_source/serializers.py @@ -24,9 +24,8 @@ from bkuser.apps.sync.constants import DataSourceSyncPeriod, SyncTaskStatus, SyncTaskTrigger from bkuser.apps.sync.models import DataSourceSyncTask from bkuser.apps.tenant.models import TenantUserCustomField, UserBuiltinField -from bkuser.biz.data_source_plugin import DefaultPluginConfigProvider from bkuser.common.constants import SENSITIVE_MASK -from bkuser.plugins.base import get_plugin_cfg_cls, is_plugin_exists +from bkuser.plugins.base import get_default_plugin_cfg, get_plugin_cfg_cls, is_plugin_exists from bkuser.plugins.constants import DataSourcePluginEnum from bkuser.plugins.local.models import PasswordRuleConfig from bkuser.plugins.models import BasePluginConfig @@ -306,9 +305,7 @@ def validate(self, attrs): except PDValidationError as e: raise ValidationError(_("密码规则配置不合法: {}").format(stringify_pydantic_error(e))) else: - attrs["password_rule"] = ( - DefaultPluginConfigProvider().get(DataSourcePluginEnum.LOCAL).password_rule.to_rule() # type: ignore - ) + attrs["password_rule"] = get_default_plugin_cfg(DataSourcePluginEnum.LOCAL).password_rule.to_rule() return attrs 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 0e57e5cf0..129888a28 100644 --- a/src/bk-user/bkuser/apis/web/data_source/views.py +++ b/src/bk-user/bkuser/apis/web/data_source/views.py @@ -49,13 +49,12 @@ from bkuser.apps.sync.data_models import DataSourceSyncOptions from bkuser.apps.sync.managers import DataSourceSyncManager from bkuser.apps.sync.models import DataSourceSyncTask -from bkuser.biz.data_source_plugin import DefaultPluginConfigProvider from bkuser.biz.exporters import DataSourceUserExporter from bkuser.common.error_codes import error_codes from bkuser.common.passwd import PasswordGenerator from bkuser.common.response import convert_workbook_to_response from bkuser.common.views import ExcludePatchAPIViewMixin, ExcludePutAPIViewMixin -from bkuser.plugins.base import get_plugin_cfg_schema_map, get_plugin_cls +from bkuser.plugins.base import get_default_plugin_cfg, get_plugin_cfg_schema_map, get_plugin_cls from bkuser.plugins.constants import DataSourcePluginEnum logger = logging.getLogger(__name__) @@ -85,8 +84,9 @@ class DataSourcePluginDefaultConfigApi(generics.RetrieveAPIView): }, ) def get(self, request, *args, **kwargs): - config = DefaultPluginConfigProvider().get(kwargs["id"]) - if not config: + try: + config = get_default_plugin_cfg(kwargs["id"]) + except NotImplementedError: raise error_codes.DATA_SOURCE_PLUGIN_NOT_DEFAULT_CONFIG return Response(DataSourcePluginDefaultConfigOutputSLZ(instance={"config": config.model_dump()}).data) diff --git a/src/bk-user/bkuser/apis/web/tenant/serializers.py b/src/bk-user/bkuser/apis/web/tenant/serializers.py index 9a9e42a72..3e880d969 100644 --- a/src/bk-user/bkuser/apis/web/tenant/serializers.py +++ b/src/bk-user/bkuser/apis/web/tenant/serializers.py @@ -22,10 +22,10 @@ from bkuser.apps.tenant.constants import TENANT_ID_REGEX from bkuser.apps.tenant.models import Tenant, TenantUser from bkuser.biz.data_source import DataSourceSimpleInfo -from bkuser.biz.data_source_plugin import DefaultPluginConfigProvider from bkuser.biz.tenant import TenantUserWithInheritedInfo from bkuser.biz.validators import validate_data_source_user_username from bkuser.common.passwd import PasswordValidator +from bkuser.plugins.base import get_default_plugin_cfg from bkuser.plugins.constants import DataSourcePluginEnum from bkuser.plugins.local.constants import PasswordGenerateMethod from bkuser.plugins.local.models import LocalDataSourcePluginConfig, NotificationConfig @@ -64,9 +64,7 @@ def validate_fixed_password(self, fixed_password: str) -> str: if not fixed_password: return fixed_password - cfg: LocalDataSourcePluginConfig = DefaultPluginConfigProvider().get( # type: ignore - DataSourcePluginEnum.LOCAL, - ) + cfg: LocalDataSourcePluginConfig = get_default_plugin_cfg(DataSourcePluginEnum.LOCAL) # type: ignore ret = PasswordValidator(cfg.password_rule.to_rule()).validate(fixed_password) # type: ignore if not ret.ok: raise ValidationError(_("固定密码的值不符合密码规则:{}").format(ret.exception_message)) diff --git a/src/bk-user/bkuser/apps/tenant/migrations/0004_init_default_tenant.py b/src/bk-user/bkuser/apps/tenant/migrations/0004_init_default_tenant.py new file mode 100644 index 000000000..afaf013ae --- /dev/null +++ b/src/bk-user/bkuser/apps/tenant/migrations/0004_init_default_tenant.py @@ -0,0 +1,78 @@ +# -*- 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. +""" +import logging +import os + +from django.db import migrations +from django.utils import timezone + +from bkuser.common.constants import PERMANENT_TIME +from bkuser.common.hashers import make_password +from bkuser.plugins.base import get_default_plugin_cfg +from bkuser.plugins.constants import DataSourcePluginEnum + +logger = logging.getLogger(__name__) + + +def forwards_func(apps, schema_editor): + """初始化本地数据源插件""" + admin_username = os.environ.get("INITIAL_ADMIN_USERNAME") + admin_password = os.environ.get("INITIAL_ADMIN_PASSWORD") + if not (admin_username and admin_password): + raise RuntimeError("INITIAL_ADMIN_USERNAME and INITIAL_ADMIN_PASSWORD must be set in environment variables") + + logger.info("start initialize default tenant & data source with admin user [%s]...", admin_username) + + Tenant = apps.get_model("tenant", "Tenant") + TenantUser = apps.get_model("tenant", "TenantUser") + TenantManager = apps.get_model("tenant", "TenantManager") + DataSource = apps.get_model("data_source", "DataSource") + DataSourceUser = apps.get_model("data_source", "DataSourceUser") + LocalDataSourceIdentityInfo = apps.get_model("data_source", "LocalDataSourceIdentityInfo") + + default_tenant = Tenant.objects.create(id="default", name="默认租户", is_default=True) + data_source = DataSource.objects.create( + name="default", + plugin_id=DataSourcePluginEnum.LOCAL, + owner_tenant_id=default_tenant.id, + plugin_config=get_default_plugin_cfg(DataSourcePluginEnum.LOCAL).model_dump(), + ) + + data_source_user = DataSourceUser.objects.create( + data_source=data_source, code=admin_username, username=admin_username, full_name=admin_username + ) + LocalDataSourceIdentityInfo.objects.create( + user=data_source_user, + password=make_password(admin_password), + password_updated_at=timezone.now(), + password_expired_at=PERMANENT_TIME, + data_source=data_source, + username=admin_username, + ) + tenant_user = TenantUser.objects.create( + tenant=default_tenant, + data_source_user=data_source_user, + data_source=data_source, + id=admin_username, + ) + TenantManager.objects.create(tenant=default_tenant, tenant_user=tenant_user) + + logger.info("initialize default tenant & data source with admin user [%s] success", admin_username) + + +class Migration(migrations.Migration): + dependencies = [ + ("tenant", "0003_auto_20231113_2017"), + ("data_source", "0002_init_builtin_data_source_plugin"), + ("idp", "0002_init_builtin_idp_plugin"), + ] + + operations = [migrations.RunPython(forwards_func)] diff --git a/src/bk-user/bkuser/biz/data_source.py b/src/bk-user/bkuser/biz/data_source.py index 4535e9c67..b5f5a0c99 100644 --- a/src/bk-user/bkuser/biz/data_source.py +++ b/src/bk-user/bkuser/biz/data_source.py @@ -21,7 +21,7 @@ DataSourcePlugin, DataSourceUserLeaderRelation, ) -from bkuser.biz.data_source_plugin import DefaultPluginConfigProvider +from bkuser.plugins.base import get_default_plugin_cfg from bkuser.plugins.constants import DataSourcePluginEnum from bkuser.plugins.local.models import LocalDataSourcePluginConfig, PasswordInitialConfig @@ -63,7 +63,7 @@ def create_local_data_source_with_merge_config( ) -> DataSource: """使用与默认配置合并后的插件配置,创建本地数据源""" plugin_id = DataSourcePluginEnum.LOCAL - plugin_config: LocalDataSourcePluginConfig = DefaultPluginConfigProvider().get(plugin_id) # type: ignore + plugin_config: LocalDataSourcePluginConfig = get_default_plugin_cfg(plugin_id) # type: ignore plugin_config.password_initial = password_initial_config return DataSource.objects.create( diff --git a/src/bk-user/bkuser/biz/data_source_plugin.py b/src/bk-user/bkuser/biz/data_source_plugin.py deleted file mode 100644 index 306e48348..000000000 --- a/src/bk-user/bkuser/biz/data_source_plugin.py +++ /dev/null @@ -1,241 +0,0 @@ -# -*- 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.plugins.constants import DataSourcePluginEnum -from bkuser.plugins.general.constants import AuthMethod, PageSize -from bkuser.plugins.general.models import AuthConfig, GeneralDataSourcePluginConfig, ServerConfig -from bkuser.plugins.local.constants import ( - NotificationMethod, - NotificationScene, - PasswordGenerateMethod, -) -from bkuser.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() - - # 通用 HTTP 数据源 - if plugin_id == DataSourcePluginEnum.GENERAL: - return self._get_default_general_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=90, - 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=( - "

您好:

" - + "

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

" - + "

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

" - + "

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

" - + "

该短信为系统自动发送,请勿回复。

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

您好:

" - + "

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

" - + "

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

" - + "

该短信为系统自动发送,请勿回复。

" - ), - ), - ], - ), - ), - 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=( - "

{{ username }},您好:

" - + "

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

" # noqa: E501 - + "

该短信为系统自动发送,请勿回复。

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

{{ username }},您好:

" - + "

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

" # noqa: E501 - + "

该短信为系统自动发送,请勿回复。

" - ), - ), - ], - ), - ), - ) - - def _get_default_general_plugin_config(self) -> BaseModel: - return GeneralDataSourcePluginConfig( - server_config=ServerConfig( - server_base_url="https://bk.example.com", - user_api_path="/api/v1/users", - department_api_path="/api/v1/departments", - page_size=PageSize.CNT_100, - request_timeout=30, - retries=3, - ), - auth_config=AuthConfig( - method=AuthMethod.BASIC_AUTH, - ), - ) diff --git a/src/bk-user/bkuser/common/hashers/pbkdf2.py b/src/bk-user/bkuser/common/hashers/pbkdf2.py index c61e285d5..8e53388e1 100644 --- a/src/bk-user/bkuser/common/hashers/pbkdf2.py +++ b/src/bk-user/bkuser/common/hashers/pbkdf2.py @@ -23,7 +23,7 @@ def _pbkdf2_hmac_sm3(password: bytes, salt: bytes, iterations: int, dk_len: int Password based key derivation function 2 (PKCS #5 v2.0) 实现参考自:lib/python3.10/hashlib.py L188 pbkdf2_hmac - TODO 性能优化,目前相同迭代次数条件下,pbkdf2_sm3_hmac 性能仅为 pbkdf2_hmac + sha256 的 1/70 + TODO 性能优化,目前相同迭代次数条件下,pbkdf2_hmac_sm3 性能仅为 pbkdf2_hmac + sha256 的 1/70 原因有二:1. tongsuopy.SM3 性能约为 hashlib.sha256 的 1/15 2. 本函数 pbkdf2_hmac 实现性能约为标准库 pbkdf2_hmac 的 1/4 注:标准库 pbkdf2_hmac 为 C 实现且带缓存,但仅支持 hashlib 内置的算法(如 sha1, sha256) diff --git a/src/bk-user/bkuser/common/passwd/generator.py b/src/bk-user/bkuser/common/passwd/generator.py index 2594339a0..7c38e53e0 100644 --- a/src/bk-user/bkuser/common/passwd/generator.py +++ b/src/bk-user/bkuser/common/passwd/generator.py @@ -9,6 +9,7 @@ specific language governing permissions and limitations under the License. """ import random +import secrets import string from django.conf import settings @@ -56,7 +57,7 @@ def _gen_charsets(self) -> str: def _gen_random_password(self) -> str: """根据字符集 + 密码长度范围,生成随机密码""" length = random.randint(self.rule.min_length, self.rule.max_length) - return "".join([random.choice(self.charsets) for _ in range(length)]) + return "".join([secrets.choice(self.charsets) for _ in range(length)]) def _validate(self, password: str) -> ValidateResult: return self.validator.validate(password) diff --git a/src/bk-user/bkuser/monitoring/healthz/probes.py b/src/bk-user/bkuser/monitoring/healthz/probes.py index 85ce1b93b..e60a9f88f 100644 --- a/src/bk-user/bkuser/monitoring/healthz/probes.py +++ b/src/bk-user/bkuser/monitoring/healthz/probes.py @@ -8,7 +8,10 @@ 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 Type + from blue_krill.monitoring.probe.mysql import MySQLProbe, transfer_django_db_settings +from blue_krill.monitoring.probe.redis import RedisProbe, RedisSentinelProbe from django.conf import settings from django.utils.module_loading import import_string @@ -20,3 +23,27 @@ def get_default_probes(): class MysqlProbe(MySQLProbe): name = "bkuser-mysql" config = transfer_django_db_settings(settings.DATABASES["default"]) + + +class _RedisProbe(RedisProbe): + name = "bkuser-redis" + redis_url = f"redis://{settings.REDIS_HOST}:{settings.REDIS_PORT}/{settings.REDIS_DB}" + + +class _RedisSentinelProbe(RedisSentinelProbe): + name = "bkuser-redis" + redis_url = ";".join( + [f"sentinel://:{settings.REDIS_PASSWORD}@{addr}/{settings.REDIS_DB}" for addr in settings.REDIS_SENTINEL_ADDR] + ) + master_name = settings.REDIS_SENTINEL_MASTER_NAME + sentinel_kwargs = {"password": settings.REDIS_SENTINEL_PASSWORD} + + +def _get_redis_probe_cls() -> Type[_RedisSentinelProbe] | Type[_RedisProbe]: + if settings.REDIS_USE_SENTINEL: + return _RedisSentinelProbe + + return _RedisProbe + + +RedisProbe = _get_redis_probe_cls() diff --git a/src/bk-user/bkuser/plugins/README.md b/src/bk-user/bkuser/plugins/README.md index face0a165..a3437c90f 100644 --- a/src/bk-user/bkuser/plugins/README.md +++ b/src/bk-user/bkuser/plugins/README.md @@ -79,9 +79,10 @@ class FoxDataSourcePlugin(BaseDataSourcePlugin): ```python from bkuser.plugins.base import register_plugin +from .defaults import DEFAULT_PLUGIN_CONFIG from .plugin import FoxDataSourcePlugin -register_plugin(FoxDataSourcePlugin) +register_plugin(FoxDataSourcePlugin, DEFAULT_PLUGIN_CONFIG) # 注意:如果这是一个自定义插件(非蓝鲸官方内置),还需要设置插件 Metadata 信息 from bkuser.plugins.models import PluginMetadata diff --git a/src/bk-user/bkuser/plugins/base.py b/src/bk-user/bkuser/plugins/base.py index 92545ca07..c09552289 100644 --- a/src/bk-user/bkuser/plugins/base.py +++ b/src/bk-user/bkuser/plugins/base.py @@ -48,9 +48,10 @@ def test_connection(self) -> TestConnectionResult: _plugin_cls_map: Dict[str | DataSourcePluginEnum, Type[BaseDataSourcePlugin]] = {} +_plugin_default_cfg_map: Dict[str | DataSourcePluginEnum, BasePluginConfig] = {} -def register_plugin(plugin_cls: Type[BaseDataSourcePlugin]): +def register_plugin(plugin_cls: Type[BaseDataSourcePlugin], default_plugin_cfg: BasePluginConfig): """注册数据源插件""" plugin_id = plugin_cls.id @@ -66,6 +67,7 @@ def register_plugin(plugin_cls: Type[BaseDataSourcePlugin]): logger.info("register data source plugin: %s", plugin_id) _plugin_cls_map[plugin_id] = plugin_cls + _plugin_default_cfg_map[plugin_id] = default_plugin_cfg def is_plugin_exists(plugin_id: str | DataSourcePluginEnum) -> bool: @@ -92,3 +94,12 @@ def get_plugin_cfg_schema_map() -> Dict[str, openapi.Schema]: f"plugin_config:{plugin_id}": gen_openapi_schema(model.config_class) for plugin_id, model in _plugin_cls_map.items() } + + +def get_default_plugin_cfg(plugin_id: str | DataSourcePluginEnum) -> BasePluginConfig: + """获取指定插件的默认配置""" + if plugin_id not in _plugin_default_cfg_map: + raise NotImplementedError(f"plugin {plugin_id} not provide default config") + + # 深复制以避免在其他逻辑中修改到默认的配置 + return _plugin_default_cfg_map[plugin_id].model_copy(deep=True) diff --git a/src/bk-user/bkuser/plugins/general/__init__.py b/src/bk-user/bkuser/plugins/general/__init__.py index df169d035..66979a590 100644 --- a/src/bk-user/bkuser/plugins/general/__init__.py +++ b/src/bk-user/bkuser/plugins/general/__init__.py @@ -11,6 +11,7 @@ from bkuser.plugins.base import register_plugin +from .defaults import DEFAULT_PLUGIN_CONFIG from .plugin import GeneralDataSourcePlugin -register_plugin(GeneralDataSourcePlugin) +register_plugin(GeneralDataSourcePlugin, DEFAULT_PLUGIN_CONFIG) diff --git a/src/bk-user/bkuser/plugins/general/defaults.py b/src/bk-user/bkuser/plugins/general/defaults.py new file mode 100644 index 000000000..a6ca496e1 --- /dev/null +++ b/src/bk-user/bkuser/plugins/general/defaults.py @@ -0,0 +1,27 @@ +# -*- 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 bkuser.plugins.general.constants import AuthMethod, PageSize +from bkuser.plugins.general.models import AuthConfig, GeneralDataSourcePluginConfig, ServerConfig + +# 通用 HTTP 数据源插件默认配置 +DEFAULT_PLUGIN_CONFIG = GeneralDataSourcePluginConfig( + server_config=ServerConfig( + server_base_url="https://bk.example.com", + user_api_path="/api/v1/users", + department_api_path="/api/v1/departments", + page_size=PageSize.CNT_100, + request_timeout=30, + retries=3, + ), + auth_config=AuthConfig( + method=AuthMethod.BASIC_AUTH, + ), +) diff --git a/src/bk-user/bkuser/plugins/local/__init__.py b/src/bk-user/bkuser/plugins/local/__init__.py index cd04006dc..9c8530c3d 100644 --- a/src/bk-user/bkuser/plugins/local/__init__.py +++ b/src/bk-user/bkuser/plugins/local/__init__.py @@ -11,6 +11,7 @@ from bkuser.plugins.base import register_plugin +from .defaults import DEFAULT_PLUGIN_CONFIG from .plugin import LocalDataSourcePlugin -register_plugin(LocalDataSourcePlugin) +register_plugin(LocalDataSourcePlugin, DEFAULT_PLUGIN_CONFIG) diff --git a/src/bk-user/bkuser/plugins/local/defaults.py b/src/bk-user/bkuser/plugins/local/defaults.py new file mode 100644 index 000000000..16c080a28 --- /dev/null +++ b/src/bk-user/bkuser/plugins/local/defaults.py @@ -0,0 +1,203 @@ +# -*- 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 bkuser.plugins.local.constants import ( + NotificationMethod, + NotificationScene, + PasswordGenerateMethod, +) +from bkuser.plugins.local.models import ( + LocalDataSourcePluginConfig, + NotificationConfig, + NotificationTemplate, + PasswordExpireConfig, + PasswordInitialConfig, + PasswordRuleConfig, +) + +# 本地数据源插件默认配置 +DEFAULT_PLUGIN_CONFIG = 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=90, + 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=( + "

您好:

" + + "

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

" + + "

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

" + + "

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

" + + "

该短信为系统自动发送,请勿回复。

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

您好:

" + + "

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

" + + "

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

" + + "

该短信为系统自动发送,请勿回复。

" + ), + ), + ], + ), + ), + 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=( + "

{{ username }},您好:

" + + "

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

" # noqa: E501 + + "

该短信为系统自动发送,请勿回复。

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

{{ username }},您好:

" + + "

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

" # noqa: E501 + + "

该短信为系统自动发送,请勿回复。

" + ), + ), + ], + ), + ), +) diff --git a/src/bk-user/bkuser/settings.py b/src/bk-user/bkuser/settings.py index 7db62f529..a08d8f438 100644 --- a/src/bk-user/bkuser/settings.py +++ b/src/bk-user/bkuser/settings.py @@ -454,6 +454,7 @@ "HEALTHZ_PROBES", default=[ "bkuser.monitoring.healthz.probes.MysqlProbe", + "bkuser.monitoring.healthz.probes.RedisProbe", ], ) 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 29b7c87c2..f17fd5f89 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 @@ -80,7 +80,7 @@ def data_source_sync_tasks(data_source) -> List[DataSourceSyncTask]: extras={"async_run": True, "overwrite": True}, ) other_tenant_task = DataSourceSyncTask.objects.create( - data_source_id=1, + data_source_id=999, status=SyncTaskStatus.SUCCESS, has_warning=False, trigger=SyncTaskTrigger.SIGNAL, diff --git a/src/bk-user/tests/test_utils/tenant.py b/src/bk-user/tests/test_utils/tenant.py index 0fd39ddff..b299aa9f4 100644 --- a/src/bk-user/tests/test_utils/tenant.py +++ b/src/bk-user/tests/test_utils/tenant.py @@ -12,7 +12,7 @@ from bkuser.apps.data_source.models import DataSource, DataSourceDepartment, DataSourceUser from bkuser.apps.tenant.models import Tenant, TenantDepartment, TenantUser -from bkuser.biz.data_source_plugin import DefaultPluginConfigProvider +from bkuser.plugins.base import get_default_plugin_cfg from bkuser.plugins.constants import DataSourcePluginEnum from bkuser.utils.uuid import generate_uuid @@ -31,7 +31,7 @@ def create_tenant(tenant_id: Optional[str] = DEFAULT_TENANT) -> Tenant: }, ) - plugin_config = DefaultPluginConfigProvider().get(DataSourcePluginEnum.LOCAL) + plugin_config = get_default_plugin_cfg(DataSourcePluginEnum.LOCAL) assert plugin_config is not None DataSource.objects.get_or_create( @@ -60,7 +60,7 @@ def create_tenant_users(tenant: Tenant, data_source_users: List[DataSourceUser]) if user.id not in existed_tenant_users ] TenantUser.objects.bulk_create(tenant_users) - return TenantUser.objects.filter(tenant=tenant) + return TenantUser.objects.filter(tenant=tenant, data_source_user__in=data_source_users) def create_tenant_departments(