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 9b7dace1a..bcf9638e2 100644 --- a/src/bk-user/bkuser/apis/web/data_source/serializers.py +++ b/src/bk-user/bkuser/apis/web/data_source/serializers.py @@ -11,6 +11,8 @@ import logging from typing import Any, Dict, List +from django.conf import settings +from django.core.files.uploadedfile import UploadedFile from django.utils.translation import gettext_lazy as _ from drf_yasg.utils import swagger_serializer_method from pydantic import ValidationError as PDValidationError @@ -20,8 +22,10 @@ from bkuser.apps.data_source.constants import FieldMappingOperation from bkuser.apps.data_source.models import DataSource, DataSourcePlugin from bkuser.apps.tenant.models import TenantUserCustomField, UserBuiltinField +from bkuser.biz.data_source_plugin import DefaultPluginConfigProvider from bkuser.plugins.base import 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 DataSourceSyncConfig from bkuser.utils.pydantic import stringify_pydantic_error @@ -174,7 +178,7 @@ class DataSourceRetrieveOutputSLZ(serializers.Serializer): class DataSourceUpdateInputSLZ(serializers.Serializer): - name = serializers.CharField(help_text="数据源名称") + name = serializers.CharField(help_text="数据源名称", max_length=128) plugin_config = serializers.JSONField(help_text="数据源插件配置") field_mapping = serializers.ListField( help_text="用户字段映射", child=DataSourceFieldMappingSLZ(), allow_empty=True, required=False, default=list @@ -267,6 +271,32 @@ class DataSourceTestConnectionOutputSLZ(serializers.Serializer): extras = serializers.JSONField(help_text="额外信息") +class DataSourceRandomPasswordInputSLZ(serializers.Serializer): + """生成随机密码""" + + password_rule_config = serializers.JSONField(help_text="密码规则配置", required=False) + + def validate(self, attrs): + passwd_rule_cfg = attrs.get("password_rule_config") + if passwd_rule_cfg: + try: + attrs["password_rule"] = PasswordRuleConfig(**passwd_rule_cfg).to_rule() + 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 + ) + + return attrs + + +class DataSourceRandomPasswordOutputSLZ(serializers.Serializer): + """生成随机密码结果""" + + password = serializers.CharField(help_text="密码") + + class LocalDataSourceImportInputSLZ(serializers.Serializer): """本地数据源导入""" @@ -274,6 +304,15 @@ class LocalDataSourceImportInputSLZ(serializers.Serializer): overwrite = serializers.BooleanField(help_text="允许对同名用户覆盖更新", default=False) incremental = serializers.BooleanField(help_text="是否使用增量同步", default=False) + def validate_file(self, file: UploadedFile) -> UploadedFile: + if not file.name.endswith(".xlsx"): + raise ValidationError(_("待导入文件必须为 Excel 格式")) + + if file.size > settings.MAX_USER_DATA_FILE_SIZE * 1024 * 1024: + raise ValidationError(_("待导入文件大小不得超过 {} M").format(settings.MAX_USER_DATA_FILE_SIZE)) + + return file + class DataSourceImportOrSyncOutputSLZ(serializers.Serializer): """数据源导入/同步结果""" 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 aba357383..3bbb75947 100644 --- a/src/bk-user/bkuser/apis/web/data_source/urls.py +++ b/src/bk-user/bkuser/apis/web/data_source/urls.py @@ -23,7 +23,9 @@ ), # 数据源创建/获取列表 path("", views.DataSourceListCreateApi.as_view(), name="data_source.list_create"), - # 连通性测试 + # 数据源随机密码获取 + path("random-passwords/", views.DataSourceRandomPasswordApi.as_view(), name="data_source.random_passwords"), + # 数据源连通性测试 path( "test-connection/", views.DataSourceTestConnectionApi.as_view(), 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 efeb5ea1e..c50fadc0c 100644 --- a/src/bk-user/bkuser/apis/web/data_source/views.py +++ b/src/bk-user/bkuser/apis/web/data_source/views.py @@ -25,6 +25,8 @@ DataSourceImportOrSyncOutputSLZ, DataSourcePluginDefaultConfigOutputSLZ, DataSourcePluginOutputSLZ, + DataSourceRandomPasswordInputSLZ, + DataSourceRandomPasswordOutputSLZ, DataSourceRetrieveOutputSLZ, DataSourceSearchInputSLZ, DataSourceSearchOutputSLZ, @@ -43,6 +45,7 @@ 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 @@ -192,6 +195,22 @@ def put(self, request, *args, **kwargs): return Response(status=status.HTTP_204_NO_CONTENT) +class DataSourceRandomPasswordApi(generics.CreateAPIView): + @swagger_auto_schema( + tags=["data_source"], + operation_description="生成数据源用户随机密码", + request_body=DataSourceRandomPasswordInputSLZ(), + responses={status.HTTP_200_OK: DataSourceRandomPasswordOutputSLZ()}, + ) + def post(self, request, *args, **kwargs): + slz = DataSourceRandomPasswordInputSLZ(data=request.data) + slz.is_valid(raise_exception=True) + data = slz.validated_data + + passwd = PasswordGenerator(data["password_rule"]).generate() + return Response(DataSourceRandomPasswordOutputSLZ(instance={"password": passwd}).data) + + class DataSourceTestConnectionApi(generics.CreateAPIView): """数据源连通性测试""" diff --git a/src/bk-user/bkuser/apps/data_source/constants.py b/src/bk-user/bkuser/apps/data_source/constants.py index 2711268a8..cf9860b7c 100644 --- a/src/bk-user/bkuser/apps/data_source/constants.py +++ b/src/bk-user/bkuser/apps/data_source/constants.py @@ -8,12 +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. """ -import re - from blue_krill.data_types.enum import EnumField, StructuredEnum from django.utils.translation import gettext_lazy as _ -DATA_SOURCE_USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]{2,31}") +from bkuser.plugins.local.constants import USERNAME_REGEX as DATA_SOURCE_USERNAME_REGEX # noqa: F401 class DataSourceStatus(str, StructuredEnum): diff --git a/src/bk-user/bkuser/biz/data_source_plugin.py b/src/bk-user/bkuser/biz/data_source_plugin.py index be135a857..256848414 100644 --- a/src/bk-user/bkuser/biz/data_source_plugin.py +++ b/src/bk-user/bkuser/biz/data_source_plugin.py @@ -52,7 +52,7 @@ def _get_default_local_plugin_config(self) -> BaseModel: not_continuous_letter=False, not_continuous_digit=False, not_repeated_symbol=False, - valid_time=365, + valid_time=90, max_retries=3, lock_time=60 * 60, ), diff --git a/src/bk-user/bkuser/biz/exporters.py b/src/bk-user/bkuser/biz/exporters.py index 294d7de7d..1f0d5146e 100644 --- a/src/bk-user/bkuser/biz/exporters.py +++ b/src/bk-user/bkuser/biz/exporters.py @@ -74,6 +74,7 @@ def export(self) -> Workbook: ) ) + self._set_all_cells_to_text_format() return self.workbook def _load_template(self): @@ -83,11 +84,8 @@ def _load_template(self): self.sheet.alignment = Alignment(wrapText=True) # 补充租户用户自定义字段 self._update_sheet_custom_field_columns() - - # 将单元格设置为纯文本模式,防止出现类型转换 - for columns in self.sheet.columns: - for cell in columns: - cell.number_format = FORMAT_TEXT + # 将所有单元格设置为文本格式 + self._set_all_cells_to_text_format() def _update_sheet_custom_field_columns(self): """在模版中补充自定义字段""" @@ -107,6 +105,12 @@ def _update_sheet_custom_field_columns(self): # 设置默认列宽 self.sheet.column_dimensions[self._gen_sheet_col_idx(col_idx)].width = self.default_column_width + def _set_all_cells_to_text_format(self): + # 将单元格设置为纯文本模式,防止出现类型转换 + for columns in self.sheet.columns: + for cell in columns: + cell.number_format = FORMAT_TEXT + @staticmethod def _gen_sheet_col_idx(idx: int) -> str: """ diff --git a/src/bk-user/bkuser/plugins/local/constants.py b/src/bk-user/bkuser/plugins/local/constants.py index 23b156114..e83d72e85 100644 --- a/src/bk-user/bkuser/plugins/local/constants.py +++ b/src/bk-user/bkuser/plugins/local/constants.py @@ -8,6 +8,7 @@ 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 re from blue_krill.data_types.enum import EnumField, StructuredEnum from django.utils.translation import gettext_lazy as _ @@ -36,6 +37,9 @@ # 保留的历史密码上限 MAX_RESERVED_PREVIOUS_PASSWORD_COUNT = 5 +# 数据源用户名规则 +USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]{2,31}") + class PasswordGenerateMethod(str, StructuredEnum): """密码生成方式""" diff --git a/src/bk-user/bkuser/settings.py b/src/bk-user/bkuser/settings.py index 9394cf934..a2c9e549f 100644 --- a/src/bk-user/bkuser/settings.py +++ b/src/bk-user/bkuser/settings.py @@ -506,7 +506,9 @@ # zxcvbn 会对密码进行总体强度评估(score [0, 4]),建议限制不能使用评分低于 3 的密码 MIN_ZXCVBN_PASSWORD_SCORE = env.int("MIN_ZXCVBN_PASSWORD_SCORE", 3) -# 数据导出配置 +# 数据导入/导出配置 +# 导入文件大小限制,单位为 MB +MAX_USER_DATA_FILE_SIZE = env.int("MAX_USER_DATA_FILE_SIZE", 10) # 导出文件名称前缀 EXPORT_EXCEL_FILENAME_PREFIX = "bk_user_export" # 成员,组织信息导出模板