Skip to content

Commit

Permalink
fix: data source import & curd accepting problems (#1307)
Browse files Browse the repository at this point in the history
  • Loading branch information
narasux authored Oct 16, 2023
1 parent 5df4170 commit edbf45a
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 12 deletions.
46 changes: 46 additions & 0 deletions src/bk-user/bkuser/apis/web/data_source/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
from bkuser.plugins.constants import DataSourcePluginEnum
from bkuser.plugins.local.models import PasswordRuleConfig
from bkuser.utils.pydantic import stringify_pydantic_error

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -146,11 +150,18 @@ class DataSourceRetrieveOutputSLZ(serializers.Serializer):


class DataSourceUpdateInputSLZ(serializers.Serializer):
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
)

def validate_name(self, name: str) -> str:
if DataSource.objects.filter(name=name).exists():
raise ValidationError(_("同名数据源已存在"))

return name

def validate_plugin_config(self, plugin_config: Dict[str, Any]) -> Dict[str, Any]:
PluginConfigCls = get_plugin_cfg_cls(self.context["plugin_id"]) # noqa: N806
# 自定义插件,可能没有对应的配置类,不需要做格式检查
Expand Down Expand Up @@ -209,13 +220,48 @@ class DataSourceTestConnectionOutputSLZ(serializers.Serializer):
department = RawDataSourceDepartmentSLZ(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):
"""本地数据源导入"""

file = serializers.FileField(help_text="数据源用户信息文件(Excel 格式)")
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 LocalDataSourceImportOutputSLZ(serializers.Serializer):
"""本地数据源导入结果"""
Expand Down
2 changes: 2 additions & 0 deletions src/bk-user/bkuser/apis/web/data_source/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
),
# 数据源创建/获取列表
path("", views.DataSourceListCreateApi.as_view(), name="data_source.list_create"),
# 数据源随机密码获取
path("random-passwords/", views.DataSourceRandomPasswordApi.as_view(), name="data_source.random_passwords"),
# 数据源更新/获取
path("<int:id>/", views.DataSourceRetrieveUpdateApi.as_view(), name="data_source.retrieve_update"),
# 数据源启/停
Expand Down
20 changes: 20 additions & 0 deletions src/bk-user/bkuser/apis/web/data_source/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
DataSourceCreateOutputSLZ,
DataSourcePluginDefaultConfigOutputSLZ,
DataSourcePluginOutputSLZ,
DataSourceRandomPasswordInputSLZ,
DataSourceRandomPasswordOutputSLZ,
DataSourceRetrieveOutputSLZ,
DataSourceSearchInputSLZ,
DataSourceSearchOutputSLZ,
Expand All @@ -42,6 +44,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
Expand Down Expand Up @@ -179,6 +182,7 @@ def put(self, request, *args, **kwargs):
data = slz.validated_data

with transaction.atomic():
data_source.name = data["name"]
data_source.plugin_config = data["plugin_config"]
data_source.field_mapping = data["field_mapping"]
data_source.updater = request.user.username
Expand All @@ -187,6 +191,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(CurrentUserTenantDataSourceMixin, generics.RetrieveAPIView):
"""数据源连通性测试"""

Expand Down
4 changes: 1 addition & 3 deletions src/bk-user/bkuser/apps/data_source/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion src/bk-user/bkuser/biz/data_source_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=30,
valid_time=90,
max_retries=3,
lock_time=60 * 60,
),
Expand Down
14 changes: 9 additions & 5 deletions src/bk-user/bkuser/biz/exporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def export(self) -> Workbook:
)
)

self._set_all_cells_to_text_format()
return self.workbook

def _load_template(self):
Expand All @@ -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):
"""在模版中补充自定义字段"""
Expand All @@ -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:
"""
Expand Down
4 changes: 4 additions & 0 deletions src/bk-user/bkuser/plugins/local/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 _
Expand Down Expand Up @@ -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):
"""密码生成方式"""
Expand Down
4 changes: 3 additions & 1 deletion src/bk-user/bkuser/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
# 成员,组织信息导出模板
Expand Down
6 changes: 4 additions & 2 deletions src/bk-user/tests/apis/web/data_source/test_data_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,21 +217,23 @@ def test_list_other_tenant_data_source(self, api_client, random_tenant, data_sou

class TestDataSourceUpdateApi:
def test_update_local_data_source(self, api_client, data_source, local_ds_plugin_config):
new_data_source_name = generate_random_string()
local_ds_plugin_config["enable_account_password_login"] = False
resp = api_client.put(
reverse("data_source.retrieve_update", kwargs={"id": data_source.id}),
data={"plugin_config": local_ds_plugin_config},
data={"name": new_data_source_name, "plugin_config": local_ds_plugin_config},
)
assert resp.status_code == status.HTTP_204_NO_CONTENT

resp = api_client.get(reverse("data_source.retrieve_update", kwargs={"id": data_source.id}))
assert resp.data["name"] == new_data_source_name
assert resp.data["plugin_config"]["enable_account_password_login"] is False

def test_update_with_invalid_plugin_config(self, api_client, data_source, local_ds_plugin_config):
local_ds_plugin_config.pop("enable_account_password_login")
resp = api_client.put(
reverse("data_source.retrieve_update", kwargs={"id": data_source.id}),
data={"plugin_config": local_ds_plugin_config},
data={"name": generate_random_string(), "plugin_config": local_ds_plugin_config},
)
assert resp.status_code == status.HTTP_400_BAD_REQUEST
assert "插件配置不合法:enable_account_password_login: Field required" in resp.data["message"]
Expand Down

0 comments on commit edbf45a

Please sign in to comment.