Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: data source import & curd accepting problems #1307

Merged
merged 4 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}")
narasux marked this conversation as resolved.
Show resolved Hide resolved


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
Loading