diff --git a/.gitignore b/.gitignore index c7b99c00b..a0054c1dd 100644 --- a/.gitignore +++ b/.gitignore @@ -216,4 +216,4 @@ pre_commit_hooks # local settings cliff.toml .codecc -.idea \ No newline at end of file +.idea 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 85db21997..b94aeaac5 100644 --- a/src/bk-user/bkuser/apis/web/data_source/serializers.py +++ b/src/bk-user/bkuser/apis/web/data_source/serializers.py @@ -11,7 +11,6 @@ import logging from typing import Any, Dict, List -from django.conf import settings from django.utils.translation import gettext_lazy as _ from drf_yasg.utils import swagger_serializer_method from pydantic import ValidationError as PDValidationError @@ -19,16 +18,8 @@ from rest_framework.exceptions import ValidationError from bkuser.apps.data_source.constants import DataSourcePluginEnum, FieldMappingOperation -from bkuser.apps.data_source.models import ( - DataSource, - DataSourceDepartment, - DataSourceDepartmentUserRelation, - DataSourcePlugin, - DataSourceUser, -) +from bkuser.apps.data_source.models import DataSource, DataSourcePlugin from bkuser.apps.data_source.plugins.constants import DATA_SOURCE_PLUGIN_CONFIG_CLASS_MAP -from bkuser.biz.validators import validate_data_source_user_username -from bkuser.common.validators import validate_phone_with_country_code from bkuser.utils.pydantic import stringify_pydantic_error logger = logging.getLogger(__name__) @@ -43,7 +34,7 @@ class DataSourceSearchOutputSLZ(serializers.Serializer): name = serializers.CharField(help_text="数据源名称") owner_tenant_id = serializers.CharField(help_text="数据源所属租户 ID") plugin_name = serializers.SerializerMethodField(help_text="数据源插件名称") - collaborative_companies = serializers.SerializerMethodField(help_text="协作公司") + cooperation_tenants = serializers.SerializerMethodField(help_text="协作公司") status = serializers.CharField(help_text="数据源状态") updater = serializers.CharField(help_text="更新者") updated_at = serializers.SerializerMethodField(help_text="更新时间") @@ -58,7 +49,7 @@ def get_plugin_name(self, obj: DataSource) -> str: allow_empty=True, ) ) - def get_collaborative_companies(self, obj: DataSource) -> List[str]: + def get_cooperation_tenants(self, obj: DataSource) -> List[str]: # TODO 目前未支持数据源跨租户协作,因此该数据均为空 return [] @@ -67,7 +58,10 @@ def get_updated_at(self, obj: DataSource) -> str: class DataSourceFieldMappingSLZ(serializers.Serializer): - """单个数据源字段映射""" + """ + 单个数据源字段映射 + FIXME (su) 动态字段实现后,需要检查:target_field 需是租户定义的,source_field 需是插件允许的 + """ source_field = serializers.CharField(help_text="数据源原始字段") mapping_operation = serializers.ChoiceField(help_text="映射关系", choices=FieldMappingOperation.get_choices()) @@ -83,6 +77,12 @@ class DataSourceCreateInputSLZ(serializers.Serializer): 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_id(self, plugin_id: str) -> str: if not DataSourcePlugin.objects.filter(id=plugin_id).exists(): raise ValidationError(_("数据源插件不存在")) @@ -159,158 +159,26 @@ def validate_field_mapping(self, field_mapping: List[Dict]) -> List[Dict]: return field_mapping -class UserSearchInputSLZ(serializers.Serializer): - username = serializers.CharField(required=False, help_text="用户名", allow_blank=True) - - -class DataSourceSearchDepartmentsOutputSLZ(serializers.Serializer): - id = serializers.CharField(help_text="部门ID") - name = serializers.CharField(help_text="部门名称") - - -class UserSearchOutputSLZ(serializers.Serializer): - id = serializers.CharField(help_text="用户ID") - username = serializers.CharField(help_text="用户名") - full_name = serializers.CharField(help_text="全名") - phone = serializers.CharField(help_text="手机号") - email = serializers.CharField(help_text="邮箱") - departments = serializers.SerializerMethodField(help_text="用户部门") - - # FIXME:考虑抽象一个函数 获取数据后传递到context - @swagger_serializer_method(serializer_or_field=DataSourceSearchDepartmentsOutputSLZ(many=True)) - def get_departments(self, obj: DataSourceUser): - return [ - {"id": department_user_relation.department.id, "name": department_user_relation.department.name} - for department_user_relation in DataSourceDepartmentUserRelation.objects.filter(user=obj) - ] - - -class UserCreateInputSLZ(serializers.Serializer): - username = serializers.CharField(help_text="用户名", validators=[validate_data_source_user_username]) - full_name = serializers.CharField(help_text="姓名") - email = serializers.EmailField(help_text="邮箱") - phone_country_code = serializers.CharField( - help_text="手机号国际区号", required=False, default=settings.DEFAULT_PHONE_COUNTRY_CODE - ) - phone = serializers.CharField(help_text="手机号") - logo = serializers.CharField(help_text="用户 Logo", required=False) - department_ids = serializers.ListField(help_text="部门ID列表", child=serializers.IntegerField(), default=[]) - leader_ids = serializers.ListField(help_text="上级ID列表", child=serializers.IntegerField(), default=[]) - - def validate(self, data): - validate_phone_with_country_code(phone=data["phone"], country_code=data["phone_country_code"]) - return data - - def validate_department_ids(self, department_ids): - diff_department_ids = set(department_ids) - set( - DataSourceDepartment.objects.filter( - id__in=department_ids, data_source=self.context["data_source"] - ).values_list("id", flat=True) - ) - if diff_department_ids: - raise serializers.ValidationError(_("传递了错误的部门信息: {}").format(diff_department_ids)) - return department_ids - - def validate_leader_ids(self, leader_ids): - diff_leader_ids = set(leader_ids) - set( - DataSourceUser.objects.filter(id__in=leader_ids, data_source=self.context["data_source"]).values_list( - "id", flat=True - ) - ) - if diff_leader_ids: - raise serializers.ValidationError(_("传递了错误的上级信息: {}").format(diff_leader_ids)) - return leader_ids - - -class UserCreateOutputSLZ(serializers.Serializer): - id = serializers.CharField(help_text="数据源用户ID") - - -class LeaderSearchInputSLZ(serializers.Serializer): - keyword = serializers.CharField(help_text="搜索关键字", required=False) - - -class LeaderSearchOutputSLZ(serializers.Serializer): - id = serializers.CharField(help_text="上级ID") - username = serializers.CharField(help_text="上级名称") - - -class DepartmentSearchInputSLZ(serializers.Serializer): - name = serializers.CharField(required=False, help_text="部门名称", allow_blank=True) +class DataSourceSwitchStatusOutputSLZ(serializers.Serializer): + status = serializers.CharField(help_text="数据源状态") -class DepartmentSearchOutputSLZ(serializers.Serializer): - id = serializers.CharField(help_text="部门ID") - name = serializers.CharField(help_text="部门名称") +class RawDataSourceUserSLZ(serializers.Serializer): + id = serializers.CharField(help_text="用户 ID") + properties = serializers.JSONField(help_text="用户属性") + leaders = serializers.ListField(help_text="用户 leader ID 列表", child=serializers.CharField()) + departments = serializers.ListField(help_text="用户部门 ID 列表", child=serializers.CharField()) -class UserDepartmentOutputSLZ(serializers.Serializer): - id = serializers.IntegerField(help_text="部门ID") +class RawDataSourceDepartmentSLZ(serializers.Serializer): + id = serializers.CharField(help_text="部门 ID") name = serializers.CharField(help_text="部门名称") + parent = serializers.CharField(help_text="父部门 ID") -class UserLeaderOutputSLZ(serializers.Serializer): - id = serializers.IntegerField(help_text="上级ID") - username = serializers.CharField(help_text="上级用户名") - - -class UserRetrieveOutputSLZ(serializers.Serializer): - username = serializers.CharField(help_text="用户名") - full_name = serializers.CharField(help_text="全名") - email = serializers.CharField(help_text="邮箱") - phone_country_code = serializers.CharField(help_text="手机区号") - phone = serializers.CharField(help_text="手机号") - logo = serializers.SerializerMethodField(help_text="用户Logo") - - departments = serializers.SerializerMethodField(help_text="部门信息") - leaders = serializers.SerializerMethodField(help_text="上级信息") - - def get_logo(self, obj: DataSourceUser) -> str: - return obj.logo or settings.DEFAULT_DATA_SOURCE_USER_LOGO +class DataSourceTestConnectionOutputSLZ(serializers.Serializer): + """数据源连通性测试""" - @swagger_serializer_method(serializer_or_field=UserDepartmentOutputSLZ(many=True)) - def get_departments(self, obj: DataSourceUser) -> List[Dict]: - user_departments_map = self.context["user_departments_map"] - departments = user_departments_map.get(obj.id, []) - return [{"id": dept.id, "name": dept.name} for dept in departments] - - @swagger_serializer_method(serializer_or_field=UserLeaderOutputSLZ(many=True)) - def get_leaders(self, obj: DataSourceUser) -> List[Dict]: - user_leaders_map = self.context["user_leaders_map"] - leaders = user_leaders_map.get(obj.id, []) - return [{"id": leader.id, "username": leader.username} for leader in leaders] - - -class UserUpdateInputSLZ(serializers.Serializer): - full_name = serializers.CharField(help_text="姓名") - email = serializers.CharField(help_text="邮箱") - phone_country_code = serializers.CharField(help_text="手机国际区号") - phone = serializers.CharField(help_text="手机号") - logo = serializers.CharField(help_text="用户 Logo", allow_blank=True) - - department_ids = serializers.ListField(help_text="部门ID列表", child=serializers.IntegerField()) - leader_ids = serializers.ListField(help_text="上级ID列表", child=serializers.IntegerField()) - - def validate(self, data): - validate_phone_with_country_code(phone=data["phone"], country_code=data["phone_country_code"]) - return data - - def validate_department_ids(self, department_ids): - diff_department_ids = set(department_ids) - set( - DataSourceDepartment.objects.filter( - id__in=department_ids, data_source=self.context["data_source"] - ).values_list("id", flat=True) - ) - if diff_department_ids: - raise serializers.ValidationError(_("传递了错误的部门信息: {}").format(diff_department_ids)) - return department_ids - - def validate_leader_ids(self, leader_ids): - diff_leader_ids = set(leader_ids) - set( - DataSourceUser.objects.filter(id__in=leader_ids, data_source=self.context["data_source"]).values_list( - "id", flat=True - ) - ) - if diff_leader_ids: - raise serializers.ValidationError(_("传递了错误的上级信息: {}").format(diff_leader_ids)) - return leader_ids + error_message = serializers.CharField(help_text="错误信息") + user = serializers.CharField(help_text="用户") + department = 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 070c04833..879fddb61 100644 --- a/src/bk-user/bkuser/apis/web/data_source/urls.py +++ b/src/bk-user/bkuser/apis/web/data_source/urls.py @@ -19,19 +19,28 @@ path("", views.DataSourceListCreateApi.as_view(), name="data_source.list_create"), # 数据源更新/获取 path("/", views.DataSourceRetrieveUpdateApi.as_view(), name="data_source.retrieve_update"), - # 通用数据源连通性测试 + # 数据源启/停 path( - "/connectivity/", - views.DataSourceConnectivityApi.as_view(), - name="data_source.connectivity_test", + "/operations/switch_status/", + views.DataSourceSwitchStatusApi.as_view(), + name="data_source.switch_status", ), - # 数据源用户 - path("/users/", views.DataSourceUserListCreateApi.as_view(), name="data_source_user.list_create"), - # 本地数据源用户导入导出 - path("/users/io/", views.DataSourceUserImportExportApi.as_view(), name="data_source_user.import_export"), - # 数据源用户 Leader - path("/leaders/", views.DataSourceLeadersListApi.as_view(), name="data_source_leaders.list"), - # 数据源部门 - path("/departments/", views.DataSourceDepartmentsListApi.as_view(), name="data_source_departments.list"), - path("user//", views.DataSourceUserRetrieveUpdateApi.as_view(), name="data_source_user.retrieve_update"), + # 连通性测试 + path( + "/operations/test_connection/", + views.DataSourceTestConnectionApi.as_view(), + name="data_source.test_connection", + ), + # 获取用户信息导入模板 + path( + "/operations/download_template/", + views.DataSourceTemplateApi.as_view(), + name="data_source.download_template", + ), + # 导出数据源用户数据 + path("/operations/export/", views.DataSourceExportApi.as_view(), name="data_source.export_data"), + # 数据源导入 + path("/operations/import/", views.DataSourceImportApi.as_view(), name="data_source.import_from_excel"), + # 手动触发数据源同步 + path("/operations/sync/", views.DataSourceSyncApi.as_view(), name="data_source.sync"), ] 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 4fedbdd44..54eb5260f 100644 --- a/src/bk-user/bkuser/apis/web/data_source/views.py +++ b/src/bk-user/bkuser/apis/web/data_source/views.py @@ -9,34 +9,38 @@ specific language governing permissions and limitations under the License. """ from django.db import transaction -from django.db.models import Q from drf_yasg.utils import swagger_auto_schema from rest_framework import generics, status from rest_framework.response import Response -from bkuser.apis.web.data_source import serializers as slzs +from bkuser.apis.web.data_source.serializers import ( + DataSourceCreateInputSLZ, + DataSourceCreateOutputSLZ, + DataSourcePluginOutputSLZ, + DataSourceRetrieveOutputSLZ, + DataSourceSearchInputSLZ, + DataSourceSearchOutputSLZ, + DataSourceSwitchStatusOutputSLZ, + DataSourceTestConnectionOutputSLZ, + DataSourceUpdateInputSLZ, +) from bkuser.apis.web.mixins import CurrentUserTenantMixin -from bkuser.apps.data_source.models import DataSource, DataSourceDepartment, DataSourcePlugin, DataSourceUser +from bkuser.apps.data_source.constants import DataSourceStatus +from bkuser.apps.data_source.models import DataSource, DataSourcePlugin from bkuser.apps.data_source.signals import post_create_data_source -from bkuser.biz.data_source_organization import ( - DataSourceOrganizationHandler, - DataSourceUserBaseInfo, - DataSourceUserEditableBaseInfo, - DataSourceUserRelationInfo, -) from bkuser.common.error_codes import error_codes -from bkuser.common.views import ExcludePatchAPIViewMixin +from bkuser.common.views import ExcludePatchAPIViewMixin, ExcludePutAPIViewMixin class DataSourcePluginListApi(generics.ListAPIView): queryset = DataSourcePlugin.objects.all() pagination_class = None - serializer_class = slzs.DataSourcePluginOutputSLZ + serializer_class = DataSourcePluginOutputSLZ @swagger_auto_schema( tags=["data_source"], operation_description="数据源插件列表", - responses={status.HTTP_200_OK: slzs.DataSourcePluginOutputSLZ(many=True)}, + responses={status.HTTP_200_OK: DataSourcePluginOutputSLZ(many=True)}, ) def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) @@ -44,13 +48,13 @@ def get(self, request, *args, **kwargs): class DataSourceListCreateApi(CurrentUserTenantMixin, generics.ListCreateAPIView): pagination_class = None - serializer_class = slzs.DataSourceSearchOutputSLZ + serializer_class = DataSourceSearchOutputSLZ def get_serializer_context(self): return {"data_source_plugin_map": dict(DataSourcePlugin.objects.values_list("id", "name"))} def get_queryset(self): - slz = slzs.DataSourceSearchInputSLZ(data=self.request.query_params) + slz = DataSourceSearchInputSLZ(data=self.request.query_params) slz.is_valid(raise_exception=True) data = slz.validated_data @@ -63,8 +67,8 @@ def get_queryset(self): @swagger_auto_schema( tags=["data_source"], operation_description="数据源列表", - query_serializer=slzs.DataSourceSearchInputSLZ(), - responses={status.HTTP_200_OK: slzs.DataSourceSearchOutputSLZ(many=True)}, + query_serializer=DataSourceSearchInputSLZ(), + responses={status.HTTP_200_OK: DataSourceSearchOutputSLZ(many=True)}, ) def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) @@ -72,37 +76,37 @@ def get(self, request, *args, **kwargs): @swagger_auto_schema( tags=["data_source"], operation_description="新建数据源", - request_body=slzs.DataSourceCreateInputSLZ(), - responses={status.HTTP_201_CREATED: slzs.DataSourceCreateOutputSLZ()}, + request_body=DataSourceCreateInputSLZ(), + responses={status.HTTP_201_CREATED: DataSourceCreateOutputSLZ()}, ) - @transaction.atomic def post(self, request, *args, **kwargs): - slz = slzs.DataSourceCreateInputSLZ(data=request.data) + slz = DataSourceCreateInputSLZ(data=request.data) slz.is_valid(raise_exception=True) data = slz.validated_data - current_user = request.user.username - ds = DataSource.objects.create( - name=data["name"], - owner_tenant_id=self.get_current_tenant_id(), - plugin=DataSourcePlugin.objects.get(id=data["plugin_id"]), - plugin_config=data["plugin_config"], - field_mapping=data["field_mapping"], - creator=current_user, - updater=current_user, - ) - # 数据源创建后,发送信号用于登录认证,用户初始化等相关工作 - post_create_data_source.send(sender=self.__class__, data_source=ds) + with transaction.atomic(): + current_user = request.user.username + ds = DataSource.objects.create( + name=data["name"], + owner_tenant_id=self.get_current_tenant_id(), + plugin=DataSourcePlugin.objects.get(id=data["plugin_id"]), + plugin_config=data["plugin_config"], + field_mapping=data["field_mapping"], + creator=current_user, + updater=current_user, + ) + # 数据源创建后,发送信号用于登录认证,用户初始化等相关工作 + post_create_data_source.send(sender=self.__class__, data_source=ds) return Response( - slzs.DataSourceCreateOutputSLZ(instance={"id": ds.id}).data, + DataSourceCreateOutputSLZ(instance={"id": ds.id}).data, status=status.HTTP_201_CREATED, ) class DataSourceRetrieveUpdateApi(CurrentUserTenantMixin, ExcludePatchAPIViewMixin, generics.RetrieveUpdateAPIView): pagination_class = None - serializer_class = slzs.DataSourceRetrieveOutputSLZ + serializer_class = DataSourceRetrieveOutputSLZ lookup_url_kwarg = "id" def get_queryset(self): @@ -111,7 +115,7 @@ def get_queryset(self): @swagger_auto_schema( tags=["data_source"], operation_description="数据源详情", - responses={status.HTTP_200_OK: slzs.DataSourceRetrieveOutputSLZ()}, + responses={status.HTTP_200_OK: DataSourceRetrieveOutputSLZ()}, ) def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) @@ -119,12 +123,12 @@ def get(self, request, *args, **kwargs): @swagger_auto_schema( tags=["data_source"], operation_description="更新数据源", - request_body=slzs.DataSourceUpdateInputSLZ(), - responses={status.HTTP_200_OK: ""}, + request_body=DataSourceUpdateInputSLZ(), + responses={status.HTTP_204_NO_CONTENT: ""}, ) def put(self, request, *args, **kwargs): data_source = self.get_object() - slz = slzs.DataSourceUpdateInputSLZ( + slz = DataSourceUpdateInputSLZ( data=request.data, context={"plugin_id": data_source.plugin_id}, ) @@ -136,212 +140,105 @@ def put(self, request, *args, **kwargs): data_source.updater = request.user.username data_source.save() - return Response() + return Response(status=status.HTTP_204_NO_CONTENT) -class DataSourceConnectivityApi(generics.RetrieveAPIView): +class DataSourceTestConnectionApi(CurrentUserTenantMixin, generics.RetrieveAPIView): """数据源连通性测试""" - def get(self, request, *args, **kwargs): - # TODO 实现代码逻辑 - return Response() - - -class DataSourceUserListCreateApi(generics.ListCreateAPIView): - serializer_class = slzs.UserSearchOutputSLZ + serializer_class = DataSourceTestConnectionOutputSLZ lookup_url_kwarg = "id" def get_queryset(self): - slz = slzs.UserSearchInputSLZ(data=self.request.query_params) - slz.is_valid(raise_exception=True) - data = slz.validated_data - data_source_id = self.kwargs["id"] - - # 校验数据源是否存在 - data_source = DataSource.objects.filter(id=data_source_id).first() - if not data_source: - raise error_codes.DATA_SOURCE_NOT_EXIST - - queryset = DataSourceUser.objects.filter(data_source=data_source) - if username := data.get("username"): - queryset = queryset.filter(username__icontains=username) - - return queryset + return DataSource.objects.filter(owner_tenant_id=self.get_current_tenant_id()) @swagger_auto_schema( tags=["data_source"], - operation_description="数据源用户列表", - query_serializer=slzs.UserSearchInputSLZ(), - responses={status.HTTP_200_OK: slzs.UserSearchOutputSLZ(many=True)}, + operation_description="数据源连通性测试", + responses={status.HTTP_200_OK: DataSourceTestConnectionOutputSLZ()}, ) def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - @swagger_auto_schema( - tags=["data_source"], - operation_description="新建数据源用户", - request_body=slzs.UserCreateInputSLZ(), - responses={status.HTTP_201_CREATED: slzs.UserCreateOutputSLZ()}, - ) - def post(self, request, *args, **kwargs): - # 校验数据源是否存在 - data_source = DataSource.objects.filter(id=self.kwargs["id"]).first() - if not data_source: - raise error_codes.DATA_SOURCE_NOT_EXIST - - slz = slzs.UserCreateInputSLZ(data=request.data, context={"data_source": data_source}) - slz.is_valid(raise_exception=True) - data = slz.validated_data - - # 不允许对非本地数据源进行用户新增操作 - if not data_source.is_local: - raise error_codes.CANNOT_CREATE_DATA_SOURCE_USER - # 校验是否已存在该用户 - if DataSourceUser.objects.filter(username=data["username"], data_source=data_source).exists(): - raise error_codes.DATA_SOURCE_USER_ALREADY_EXISTED - - # 用户数据整合 - base_user_info = DataSourceUserBaseInfo( - username=data["username"], - full_name=data["full_name"], - email=data["email"], - phone=data["phone"], - phone_country_code=data["phone_country_code"], - ) - - relation_info = DataSourceUserRelationInfo( - department_ids=data["department_ids"], leader_ids=data["leader_ids"] - ) - - user_id = DataSourceOrganizationHandler.create_user( - data_source=data_source, base_user_info=base_user_info, relation_info=relation_info - ) - return Response(slzs.UserCreateOutputSLZ(instance={"id": user_id}).data) - - -class DataSourceLeadersListApi(generics.ListAPIView): - serializer_class = slzs.LeaderSearchOutputSLZ + data_source = self.get_object() + if data_source.is_local: + raise error_codes.DATA_SOURCE_OPERATION_UNSUPPORTED + + # TODO (su) 实现代码逻辑,需调用数据源插件以确认连通性 + mock_data = { + "error_message": "", + "user": { + "id": "uid_2", + "properties": { + "username": "zhangSan", + }, + "leaders": ["uid_0", "uid_1"], + "departments": ["dept_id_1"], + }, + "department": { + "id": "dept_id_1", + "name": "dept_name", + "parent": "dept_id_0", + }, + } + + return Response(DataSourceTestConnectionOutputSLZ(instance=mock_data).data) + + +class DataSourceSwitchStatusApi(CurrentUserTenantMixin, ExcludePutAPIViewMixin, generics.UpdateAPIView): + """切换数据源状态(启/停)""" + + serializer_class = DataSourceSwitchStatusOutputSLZ + lookup_url_kwarg = "id" def get_queryset(self): - slz = slzs.LeaderSearchInputSLZ(data=self.request.query_params) - slz.is_valid(raise_exception=True) - data = slz.validated_data - - # 校验数据源是否存在 - data_source = DataSource.objects.filter(id=self.kwargs["id"]).first() - if not data_source: - raise error_codes.DATA_SOURCE_NOT_EXIST - - queryset = DataSourceUser.objects.filter(data_source=data_source) - if keyword := data.get("keyword"): - queryset = queryset.filter(Q(username__icontains=keyword) | Q(full_name__icontains=keyword)) - - return queryset + return DataSource.objects.filter(owner_tenant_id=self.get_current_tenant_id()) @swagger_auto_schema( tags=["data_source"], - operation_description="数据源用户上级列表", - query_serializer=slzs.LeaderSearchInputSLZ(), - responses={status.HTTP_200_OK: slzs.LeaderSearchOutputSLZ(many=True)}, + operation_description="变更数据源状态", + responses={status.HTTP_200_OK: DataSourceSwitchStatusOutputSLZ()}, ) - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - -class DataSourceDepartmentsListApi(generics.ListAPIView): - serializer_class = slzs.DepartmentSearchOutputSLZ - - def get_queryset(self): - slz = slzs.DepartmentSearchInputSLZ(data=self.request.query_params) - slz.is_valid(raise_exception=True) - data = slz.validated_data + def patch(self, request, *args, **kwargs): + data_source = self.get_object() + if data_source.status == DataSourceStatus.ENABLED: + data_source.status = DataSourceStatus.DISABLED + else: + data_source.status = DataSourceStatus.ENABLED - # 校验数据源是否存在 - data_source = DataSource.objects.filter(id=self.kwargs["id"]).first() - if not data_source: - raise error_codes.DATA_SOURCE_NOT_EXIST + data_source.updater = request.user.username + data_source.save(update_fields=["status", "updater", "updated_at"]) - queryset = DataSourceDepartment.objects.filter(data_source=data_source) + return Response(DataSourceSwitchStatusOutputSLZ(instance={"status": data_source.status.value}).data) - if name := data.get("name"): - queryset = queryset.filter(name__icontains=name) - return queryset - - @swagger_auto_schema( - tags=["data_source"], - operation_description="数据源部门列表", - query_serializer=slzs.DepartmentSearchInputSLZ(), - responses={status.HTTP_200_OK: slzs.DepartmentSearchOutputSLZ(many=True)}, - ) +class DataSourceTemplateApi(generics.ListAPIView): def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) + """数据源导出模板""" + # TODO (su) 实现代码逻辑 + return Response() -class DataSourceUserImportExportApi(generics.ListCreateAPIView): - """本地数据源用户导入导出""" +class DataSourceExportApi(generics.ListAPIView): + """本地数据源用户导出""" def get(self, request, *args, **kwargs): """导出指定的本地数据源用户数据(Excel 格式)""" - # TODO 实现代码逻辑 - return Response() - - def post(self, request, *args, **kwargs): - """导入本地数据源用户数据(Excel 格式)""" - # TODO 实现代码逻辑 + # TODO (su) 实现代码逻辑,注意:仅本地数据源可以导出 return Response() -class DataSourceUserRetrieveUpdateApi(ExcludePatchAPIViewMixin, generics.RetrieveUpdateAPIView): - queryset = DataSourceUser.objects.all() - lookup_url_kwarg = "id" - serializer_class = slzs.UserRetrieveOutputSLZ - - def get_serializer_context(self): - user_departments_map = DataSourceOrganizationHandler.get_user_departments_map_by_user_id( - user_ids=[self.kwargs["id"]] - ) - user_leaders_map = DataSourceOrganizationHandler.get_user_leaders_map_by_user_id([self.kwargs["id"]]) - return {"user_departments_map": user_departments_map, "user_leaders_map": user_leaders_map} - - @swagger_auto_schema( - operation_description="数据源用户详情", - responses={status.HTTP_200_OK: slzs.UserRetrieveOutputSLZ()}, - tags=["data_source"], - ) - def get(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) +class DataSourceImportApi(generics.CreateAPIView): + """从 Excel 导入数据源用户数据""" - @swagger_auto_schema( - operation_description="更新数据源用户", - request_body=slzs.UserUpdateInputSLZ(), - responses={status.HTTP_200_OK: ""}, - tags=["data_source"], - ) - def put(self, request, *args, **kwargs): - user = self.get_object() - if not user.data_source.is_local: - raise error_codes.CANNOT_UPDATE_DATA_SOURCE_USER - - slz = slzs.UserUpdateInputSLZ(data=request.data, context={"data_source": user.data_source}) - slz.is_valid(raise_exception=True) - data = slz.validated_data - - # 用户数据整合 - base_user_info = DataSourceUserEditableBaseInfo( - full_name=data["full_name"], - email=data["email"], - phone_country_code=data["phone_country_code"], - phone=data["phone"], - logo=data["logo"], - ) + def post(self, request, *args, **kwargs): + """从 Excel 导入数据源用户数据""" + # TODO (su) 实现代码逻辑,注意:仅本地数据源可以导入 + return Response() - relation_info = DataSourceUserRelationInfo( - department_ids=data["department_ids"], leader_ids=data["leader_ids"] - ) - DataSourceOrganizationHandler.update_user( - user=user, base_user_info=base_user_info, relation_info=relation_info - ) +class DataSourceSyncApi(generics.CreateAPIView): + """数据源同步""" + def post(self, request, *args, **kwargs): + """触发数据源同步任务""" + # TODO (su) 实现代码逻辑,注意:本地数据源应该使用导入,而不是同步 return Response() diff --git a/src/bk-user/bkuser/apps/data_source/handlers.py b/src/bk-user/bkuser/apis/web/data_source_organization/__init__.py similarity index 61% rename from src/bk-user/bkuser/apps/data_source/handlers.py rename to src/bk-user/bkuser/apis/web/data_source_organization/__init__.py index 47f2d67c8..1060b7bf4 100644 --- a/src/bk-user/bkuser/apps/data_source/handlers.py +++ b/src/bk-user/bkuser/apis/web/data_source_organization/__init__.py @@ -8,17 +8,3 @@ 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 - -from django.dispatch import receiver - -from bkuser.apps.data_source.models import DataSource -from bkuser.apps.data_source.signals import post_create_data_source - -logger = logging.getLogger(__name__) - - -@receiver(post_create_data_source) -def after_data_source_created(sender, data_source: DataSource, **kwargs): - """TODO 数据源创建后,需要执行相关初始化工作""" - logger.info("receive post_create_data_source signal") diff --git a/src/bk-user/bkuser/apis/web/data_source_organization/serializers.py b/src/bk-user/bkuser/apis/web/data_source_organization/serializers.py new file mode 100644 index 000000000..bfa9bf323 --- /dev/null +++ b/src/bk-user/bkuser/apis/web/data_source_organization/serializers.py @@ -0,0 +1,184 @@ +# -*- 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 +from typing import Dict, List + +from django.conf import settings +from django.utils.translation import gettext_lazy as _ +from drf_yasg.utils import swagger_serializer_method +from rest_framework import serializers + +from bkuser.apps.data_source.models import ( + DataSourceDepartment, + DataSourceDepartmentUserRelation, + DataSourceUser, +) +from bkuser.biz.validators import validate_data_source_user_username +from bkuser.common.validators import validate_phone_with_country_code + +logger = logging.getLogger(__name__) + + +class UserSearchInputSLZ(serializers.Serializer): + username = serializers.CharField(required=False, help_text="用户名", allow_blank=True) + + +class DataSourceSearchDepartmentsOutputSLZ(serializers.Serializer): + id = serializers.CharField(help_text="部门ID") + name = serializers.CharField(help_text="部门名称") + + +class UserSearchOutputSLZ(serializers.Serializer): + id = serializers.CharField(help_text="用户ID") + username = serializers.CharField(help_text="用户名") + full_name = serializers.CharField(help_text="全名") + phone = serializers.CharField(help_text="手机号") + email = serializers.CharField(help_text="邮箱") + departments = serializers.SerializerMethodField(help_text="用户部门") + + # FIXME:考虑抽象一个函数 获取数据后传递到context + @swagger_serializer_method(serializer_or_field=DataSourceSearchDepartmentsOutputSLZ(many=True)) + def get_departments(self, obj: DataSourceUser): + return [ + {"id": department_user_relation.department.id, "name": department_user_relation.department.name} + for department_user_relation in DataSourceDepartmentUserRelation.objects.filter(user=obj) + ] + + +class UserCreateInputSLZ(serializers.Serializer): + username = serializers.CharField(help_text="用户名", validators=[validate_data_source_user_username]) + full_name = serializers.CharField(help_text="姓名") + email = serializers.EmailField(help_text="邮箱") + phone_country_code = serializers.CharField( + help_text="手机号国际区号", required=False, default=settings.DEFAULT_PHONE_COUNTRY_CODE + ) + phone = serializers.CharField(help_text="手机号") + logo = serializers.CharField(help_text="用户 Logo", required=False) + department_ids = serializers.ListField(help_text="部门ID列表", child=serializers.IntegerField(), default=[]) + leader_ids = serializers.ListField(help_text="上级ID列表", child=serializers.IntegerField(), default=[]) + + def validate(self, data): + validate_phone_with_country_code(phone=data["phone"], country_code=data["phone_country_code"]) + return data + + def validate_department_ids(self, department_ids): + diff_department_ids = set(department_ids) - set( + DataSourceDepartment.objects.filter( + id__in=department_ids, data_source=self.context["data_source"] + ).values_list("id", flat=True) + ) + if diff_department_ids: + raise serializers.ValidationError(_("传递了错误的部门信息: {}").format(diff_department_ids)) + return department_ids + + def validate_leader_ids(self, leader_ids): + diff_leader_ids = set(leader_ids) - set( + DataSourceUser.objects.filter(id__in=leader_ids, data_source=self.context["data_source"]).values_list( + "id", flat=True + ) + ) + if diff_leader_ids: + raise serializers.ValidationError(_("传递了错误的上级信息: {}").format(diff_leader_ids)) + return leader_ids + + +class UserCreateOutputSLZ(serializers.Serializer): + id = serializers.CharField(help_text="数据源用户ID") + + +class LeaderSearchInputSLZ(serializers.Serializer): + keyword = serializers.CharField(help_text="搜索关键字", required=False) + + +class LeaderSearchOutputSLZ(serializers.Serializer): + id = serializers.CharField(help_text="上级ID") + username = serializers.CharField(help_text="上级名称") + + +class DepartmentSearchInputSLZ(serializers.Serializer): + name = serializers.CharField(required=False, help_text="部门名称", allow_blank=True) + + +class DepartmentSearchOutputSLZ(serializers.Serializer): + id = serializers.CharField(help_text="部门ID") + name = serializers.CharField(help_text="部门名称") + + +class UserDepartmentOutputSLZ(serializers.Serializer): + id = serializers.IntegerField(help_text="部门ID") + name = serializers.CharField(help_text="部门名称") + + +class UserLeaderOutputSLZ(serializers.Serializer): + id = serializers.IntegerField(help_text="上级ID") + username = serializers.CharField(help_text="上级用户名") + + +class UserRetrieveOutputSLZ(serializers.Serializer): + username = serializers.CharField(help_text="用户名") + full_name = serializers.CharField(help_text="全名") + email = serializers.CharField(help_text="邮箱") + phone_country_code = serializers.CharField(help_text="手机区号") + phone = serializers.CharField(help_text="手机号") + logo = serializers.SerializerMethodField(help_text="用户Logo") + + departments = serializers.SerializerMethodField(help_text="部门信息") + leaders = serializers.SerializerMethodField(help_text="上级信息") + + def get_logo(self, obj: DataSourceUser) -> str: + return obj.logo or settings.DEFAULT_DATA_SOURCE_USER_LOGO + + @swagger_serializer_method(serializer_or_field=UserDepartmentOutputSLZ(many=True)) + def get_departments(self, obj: DataSourceUser) -> List[Dict]: + user_departments_map = self.context["user_departments_map"] + departments = user_departments_map.get(obj.id, []) + return [{"id": dept.id, "name": dept.name} for dept in departments] + + @swagger_serializer_method(serializer_or_field=UserLeaderOutputSLZ(many=True)) + def get_leaders(self, obj: DataSourceUser) -> List[Dict]: + user_leaders_map = self.context["user_leaders_map"] + leaders = user_leaders_map.get(obj.id, []) + return [{"id": leader.id, "username": leader.username} for leader in leaders] + + +class UserUpdateInputSLZ(serializers.Serializer): + full_name = serializers.CharField(help_text="姓名") + email = serializers.CharField(help_text="邮箱") + phone_country_code = serializers.CharField(help_text="手机国际区号") + phone = serializers.CharField(help_text="手机号") + logo = serializers.CharField(help_text="用户 Logo", allow_blank=True) + + department_ids = serializers.ListField(help_text="部门ID列表", child=serializers.IntegerField()) + leader_ids = serializers.ListField(help_text="上级ID列表", child=serializers.IntegerField()) + + def validate(self, data): + validate_phone_with_country_code(phone=data["phone"], country_code=data["phone_country_code"]) + return data + + def validate_department_ids(self, department_ids): + diff_department_ids = set(department_ids) - set( + DataSourceDepartment.objects.filter( + id__in=department_ids, data_source=self.context["data_source"] + ).values_list("id", flat=True) + ) + if diff_department_ids: + raise serializers.ValidationError(_("传递了错误的部门信息: {}").format(diff_department_ids)) + return department_ids + + def validate_leader_ids(self, leader_ids): + diff_leader_ids = set(leader_ids) - set( + DataSourceUser.objects.filter(id__in=leader_ids, data_source=self.context["data_source"]).values_list( + "id", flat=True + ) + ) + if diff_leader_ids: + raise serializers.ValidationError(_("传递了错误的上级信息: {}").format(diff_leader_ids)) + return leader_ids diff --git a/src/bk-user/bkuser/apis/web/data_source_organization/urls.py b/src/bk-user/bkuser/apis/web/data_source_organization/urls.py new file mode 100644 index 000000000..9db730e2e --- /dev/null +++ b/src/bk-user/bkuser/apis/web/data_source_organization/urls.py @@ -0,0 +1,23 @@ +# -*- 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 django.urls import path + +from bkuser.apis.web.data_source_organization import views + +urlpatterns = [ + # 数据源用户 + path("/users/", views.DataSourceUserListCreateApi.as_view(), name="data_source_user.list_create"), + # 数据源用户 Leader + path("/leaders/", views.DataSourceLeadersListApi.as_view(), name="data_source_leaders.list"), + # 数据源部门 + path("/departments/", views.DataSourceDepartmentsListApi.as_view(), name="data_source_departments.list"), + path("users//", views.DataSourceUserRetrieveUpdateApi.as_view(), name="data_source_user.retrieve_update"), +] diff --git a/src/bk-user/bkuser/apis/web/data_source_organization/views.py b/src/bk-user/bkuser/apis/web/data_source_organization/views.py new file mode 100644 index 000000000..1998d9a89 --- /dev/null +++ b/src/bk-user/bkuser/apis/web/data_source_organization/views.py @@ -0,0 +1,220 @@ +# -*- 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 django.db.models import Q +from drf_yasg.utils import swagger_auto_schema +from rest_framework import generics, status +from rest_framework.response import Response + +from bkuser.apis.web.data_source_organization.serializers import ( + DepartmentSearchInputSLZ, + DepartmentSearchOutputSLZ, + LeaderSearchInputSLZ, + LeaderSearchOutputSLZ, + UserCreateInputSLZ, + UserCreateOutputSLZ, + UserRetrieveOutputSLZ, + UserSearchInputSLZ, + UserSearchOutputSLZ, + UserUpdateInputSLZ, +) +from bkuser.apps.data_source.models import DataSource, DataSourceDepartment, DataSourceUser +from bkuser.biz.data_source_organization import ( + DataSourceOrganizationHandler, + DataSourceUserBaseInfo, + DataSourceUserEditableBaseInfo, + DataSourceUserRelationInfo, +) +from bkuser.common.error_codes import error_codes +from bkuser.common.views import ExcludePatchAPIViewMixin + + +class DataSourceUserListCreateApi(generics.ListCreateAPIView): + serializer_class = UserSearchOutputSLZ + lookup_url_kwarg = "id" + + def get_queryset(self): + slz = UserSearchInputSLZ(data=self.request.query_params) + slz.is_valid(raise_exception=True) + data = slz.validated_data + data_source_id = self.kwargs["id"] + + # 校验数据源是否存在 + data_source = DataSource.objects.filter(id=data_source_id).first() + if not data_source: + raise error_codes.DATA_SOURCE_NOT_EXIST + + queryset = DataSourceUser.objects.filter(data_source=data_source) + if username := data.get("username"): + queryset = queryset.filter(username__icontains=username) + + return queryset + + @swagger_auto_schema( + tags=["data_source"], + operation_description="数据源用户列表", + query_serializer=UserSearchInputSLZ(), + responses={status.HTTP_200_OK: UserSearchOutputSLZ(many=True)}, + ) + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + @swagger_auto_schema( + tags=["data_source"], + operation_description="新建数据源用户", + request_body=UserCreateInputSLZ(), + responses={status.HTTP_201_CREATED: UserCreateOutputSLZ()}, + ) + def post(self, request, *args, **kwargs): + # 校验数据源是否存在 + data_source = DataSource.objects.filter(id=self.kwargs["id"]).first() + if not data_source: + raise error_codes.DATA_SOURCE_NOT_EXIST + + slz = UserCreateInputSLZ(data=request.data, context={"data_source": data_source}) + slz.is_valid(raise_exception=True) + data = slz.validated_data + + # 不允许对非本地数据源进行用户新增操作 + if not data_source.is_local: + raise error_codes.CANNOT_CREATE_DATA_SOURCE_USER + # 校验是否已存在该用户 + if DataSourceUser.objects.filter(username=data["username"], data_source=data_source).exists(): + raise error_codes.DATA_SOURCE_USER_ALREADY_EXISTED + + # 用户数据整合 + base_user_info = DataSourceUserBaseInfo( + username=data["username"], + full_name=data["full_name"], + email=data["email"], + phone=data["phone"], + phone_country_code=data["phone_country_code"], + ) + + relation_info = DataSourceUserRelationInfo( + department_ids=data["department_ids"], leader_ids=data["leader_ids"] + ) + + user_id = DataSourceOrganizationHandler.create_user( + data_source=data_source, base_user_info=base_user_info, relation_info=relation_info + ) + return Response(UserCreateOutputSLZ(instance={"id": user_id}).data) + + +class DataSourceLeadersListApi(generics.ListAPIView): + serializer_class = LeaderSearchOutputSLZ + + def get_queryset(self): + slz = LeaderSearchInputSLZ(data=self.request.query_params) + slz.is_valid(raise_exception=True) + data = slz.validated_data + + # 校验数据源是否存在 + data_source = DataSource.objects.filter(id=self.kwargs["id"]).first() + if not data_source: + raise error_codes.DATA_SOURCE_NOT_EXIST + + queryset = DataSourceUser.objects.filter(data_source=data_source) + if keyword := data.get("keyword"): + queryset = queryset.filter(Q(username__icontains=keyword) | Q(full_name__icontains=keyword)) + + return queryset + + @swagger_auto_schema( + tags=["data_source"], + operation_description="数据源用户上级列表", + query_serializer=LeaderSearchInputSLZ(), + responses={status.HTTP_200_OK: LeaderSearchOutputSLZ(many=True)}, + ) + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + +class DataSourceDepartmentsListApi(generics.ListAPIView): + serializer_class = DepartmentSearchOutputSLZ + + def get_queryset(self): + slz = DepartmentSearchInputSLZ(data=self.request.query_params) + slz.is_valid(raise_exception=True) + data = slz.validated_data + + # 校验数据源是否存在 + data_source = DataSource.objects.filter(id=self.kwargs["id"]).first() + if not data_source: + raise error_codes.DATA_SOURCE_NOT_EXIST + + queryset = DataSourceDepartment.objects.filter(data_source=data_source) + + if name := data.get("name"): + queryset = queryset.filter(name__icontains=name) + + return queryset + + @swagger_auto_schema( + tags=["data_source"], + operation_description="数据源部门列表", + query_serializer=DepartmentSearchInputSLZ(), + responses={status.HTTP_200_OK: DepartmentSearchOutputSLZ(many=True)}, + ) + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + +class DataSourceUserRetrieveUpdateApi(ExcludePatchAPIViewMixin, generics.RetrieveUpdateAPIView): + queryset = DataSourceUser.objects.all() + lookup_url_kwarg = "id" + serializer_class = UserRetrieveOutputSLZ + + def get_serializer_context(self): + user_departments_map = DataSourceOrganizationHandler.get_user_departments_map_by_user_id( + user_ids=[self.kwargs["id"]] + ) + user_leaders_map = DataSourceOrganizationHandler.get_user_leaders_map_by_user_id([self.kwargs["id"]]) + return {"user_departments_map": user_departments_map, "user_leaders_map": user_leaders_map} + + @swagger_auto_schema( + tags=["data_source"], + operation_description="数据源用户详情", + responses={status.HTTP_200_OK: UserRetrieveOutputSLZ()}, + ) + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + @swagger_auto_schema( + tags=["data_source"], + operation_description="更新数据源用户", + request_body=UserUpdateInputSLZ(), + responses={status.HTTP_204_NO_CONTENT: ""}, + ) + def put(self, request, *args, **kwargs): + user = self.get_object() + if not user.data_source.is_local: + raise error_codes.CANNOT_UPDATE_DATA_SOURCE_USER + + slz = UserUpdateInputSLZ(data=request.data, context={"data_source": user.data_source}) + slz.is_valid(raise_exception=True) + data = slz.validated_data + + # 用户数据整合 + base_user_info = DataSourceUserEditableBaseInfo( + full_name=data["full_name"], + email=data["email"], + phone_country_code=data["phone_country_code"], + phone=data["phone"], + logo=data["logo"], + ) + relation_info = DataSourceUserRelationInfo( + department_ids=data["department_ids"], leader_ids=data["leader_ids"] + ) + DataSourceOrganizationHandler.update_user( + user=user, base_user_info=base_user_info, relation_info=relation_info + ) + + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/src/bk-user/bkuser/apis/web/tenant/serializers.py b/src/bk-user/bkuser/apis/web/tenant/serializers.py index f4022bbda..98231846c 100644 --- a/src/bk-user/bkuser/apis/web/tenant/serializers.py +++ b/src/bk-user/bkuser/apis/web/tenant/serializers.py @@ -42,7 +42,7 @@ class TenantCreateInputSLZ(serializers.Serializer): logo = serializers.CharField(help_text="租户 Logo", required=False) managers = serializers.ListField(help_text="管理人列表", child=TenantManagerCreateInputSLZ(), allow_empty=False) feature_flags = TenantFeatureFlagSLZ(help_text="租户特性集") - # TODO: 目前还没设计数据源,待开发本地数据源时再补充 + # FIXME (su): 目前还没设计数据源,待开发本地数据源时再补充 # password_config diff --git a/src/bk-user/bkuser/apis/web/urls.py b/src/bk-user/bkuser/apis/web/urls.py index e9c37eb17..0925c2a2a 100644 --- a/src/bk-user/bkuser/apis/web/urls.py +++ b/src/bk-user/bkuser/apis/web/urls.py @@ -17,4 +17,5 @@ path("tenants/", include("bkuser.apis.web.tenant.urls")), path("tenant-organization/", include("bkuser.apis.web.organization.urls")), path("data-sources/", include("bkuser.apis.web.data_source.urls")), + path("data-sources/", include("bkuser.apis.web.data_source_organization.urls")), ] diff --git a/src/bk-user/bkuser/apps/data_source/plugins/base.py b/src/bk-user/bkuser/apps/data_source/plugins/base.py index 2c24fc801..32d2e66c8 100644 --- a/src/bk-user/bkuser/apps/data_source/plugins/base.py +++ b/src/bk-user/bkuser/apps/data_source/plugins/base.py @@ -11,7 +11,7 @@ from abc import ABC, abstractmethod from typing import List -from bkuser.apps.data_source.plugins.models import RawDataSourceDepartment, RawDataSourceUser +from bkuser.apps.data_source.plugins.models import RawDataSourceDepartment, RawDataSourceUser, TestConnectionResult class BaseDataSourcePlugin(ABC): @@ -26,3 +26,8 @@ def fetch_departments(self) -> List[RawDataSourceDepartment]: def fetch_users(self) -> List[RawDataSourceUser]: """获取用户信息""" ... + + @abstractmethod + def test_connection(self) -> TestConnectionResult: + """连通性测试(非本地数据源需提供)""" + ... diff --git a/src/bk-user/bkuser/apps/data_source/plugins/local/plugin.py b/src/bk-user/bkuser/apps/data_source/plugins/local/plugin.py index 2d943d454..c5f46d64f 100644 --- a/src/bk-user/bkuser/apps/data_source/plugins/local/plugin.py +++ b/src/bk-user/bkuser/apps/data_source/plugins/local/plugin.py @@ -10,9 +10,11 @@ """ from typing import List +from django.utils.translation import ugettext_lazy as _ + from bkuser.apps.data_source.plugins.base import BaseDataSourcePlugin from bkuser.apps.data_source.plugins.local.models import LocalDataSourcePluginConfig -from bkuser.apps.data_source.plugins.models import RawDataSourceDepartment, RawDataSourceUser +from bkuser.apps.data_source.plugins.models import RawDataSourceDepartment, RawDataSourceUser, TestConnectionResult class LocalDataSourcePlugin(BaseDataSourcePlugin): @@ -28,3 +30,6 @@ def fetch_departments(self) -> List[RawDataSourceDepartment]: def fetch_users(self) -> List[RawDataSourceUser]: """获取用户信息""" return [] + + def test_connection(self) -> TestConnectionResult: + raise NotImplementedError(_("本地数据源不支持连通性测试")) diff --git a/src/bk-user/bkuser/apps/data_source/plugins/models.py b/src/bk-user/bkuser/apps/data_source/plugins/models.py index d22a18941..e4f99aedf 100644 --- a/src/bk-user/bkuser/apps/data_source/plugins/models.py +++ b/src/bk-user/bkuser/apps/data_source/plugins/models.py @@ -35,3 +35,11 @@ class RawDataSourceDepartment(BaseModel): name: str # 上级部门 parent: str + + +class TestConnectionResult(BaseModel): + """连通性测试结果,包含示例数据""" + + error_message: str + user: RawDataSourceUser + department: RawDataSourceDepartment diff --git a/src/bk-user/bkuser/biz/tenant.py b/src/bk-user/bkuser/biz/tenant.py index d056cfe65..b7ba4e0e0 100644 --- a/src/bk-user/bkuser/biz/tenant.py +++ b/src/bk-user/bkuser/biz/tenant.py @@ -266,7 +266,7 @@ def create_with_managers(tenant_info: TenantBaseInfo, managers: List[TenantManag # 创建租户本身 tenant = Tenant.objects.create(**tenant_info.model_dump()) - # FIXME: 开发本地数据源时,重写(直接调用本地数据源Handler) + # FIXME (su): 开发本地数据源时,重写(直接调用本地数据源Handler) # 创建本地数据源,名称则使用租户名称 data_source = DataSource.objects.create( name=f"{tenant_info.name}-本地数据源", diff --git a/src/bk-user/bkuser/common/error_codes.py b/src/bk-user/bkuser/common/error_codes.py index 0a9f898a7..5ff9b61c5 100644 --- a/src/bk-user/bkuser/common/error_codes.py +++ b/src/bk-user/bkuser/common/error_codes.py @@ -72,7 +72,7 @@ class ErrorCodes: # 调用外部系统API REMOTE_REQUEST_ERROR = ErrorCode(_("调用外部系统API异常")) # 数据源 - DATA_SOURCE_TYPE_NOT_SUPPORTED = ErrorCode(_("数据源类型不支持")) + DATA_SOURCE_OPERATION_UNSUPPORTED = ErrorCode(_("数据源不支持该操作")) DATA_SOURCE_NOT_EXIST = ErrorCode(_("数据源不存在")) CANNOT_CREATE_DATA_SOURCE_USER = ErrorCode(_("该数据源不支持新增用户")) CANNOT_UPDATE_DATA_SOURCE_USER = 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 eaca63938..e3abe6eab 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 @@ -157,7 +157,7 @@ def test_list_with_keyword(self, api_client, data_source): assert ds["owner_tenant_id"] == data_source.owner_tenant_id assert ds["plugin_name"] == DataSourcePluginEnum.get_choice_label(DataSourcePluginEnum.LOCAL) assert ds["status"] == DataSourceStatus.ENABLED - assert ds["collaborative_companies"] == [] + assert ds["cooperation_tenants"] == [] def test_list_other_tenant_data_source(self, api_client, random_tenant, data_source): resp = api_client.get(reverse("data_source.list_create"), data={"keyword": data_source.name}) @@ -172,7 +172,7 @@ def test_update_local_data_source(self, api_client, data_source, local_ds_plugin reverse("data_source.retrieve_update", kwargs={"id": data_source.id}), data={"plugin_config": local_ds_plugin_config}, ) - assert resp.status_code == status.HTTP_200_OK + 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["plugin_config"]["enable_login_by_password"] is False @@ -208,3 +208,17 @@ def test_retrieve_other_tenant_data_source(self, api_client, random_tenant, data resp = api_client.get(reverse("data_source.retrieve_update", kwargs={"id": data_source.id})) # 无法查看到其他租户的数据源信息 assert resp.status_code == status.HTTP_404_NOT_FOUND + + +class TestDataSourceSwitchStatusApi: + def test_switch(self, api_client, data_source): + url = reverse("data_source.switch_status", kwargs={"id": data_source.id}) + # 默认启用,切换后不可用 + assert api_client.patch(url).data["status"] == DataSourceStatus.DISABLED + # 再次切换,变成可用 + assert api_client.patch(url).data["status"] == DataSourceStatus.ENABLED + + def test_patch_other_tenant_data_source(self, api_client, random_tenant, data_source): + resp = api_client.patch(reverse("data_source.switch_status", kwargs={"id": data_source.id})) + # 无法操作其他租户的数据源信息 + assert resp.status_code == status.HTTP_404_NOT_FOUND