diff --git a/src/bk-user/bkuser/apis/web/organization/serializers.py b/src/bk-user/bkuser/apis/web/organization/serializers.py index f796fba7a..6dd948078 100644 --- a/src/bk-user/bkuser/apis/web/organization/serializers.py +++ b/src/bk-user/bkuser/apis/web/organization/serializers.py @@ -10,11 +10,36 @@ """ import logging +from django.conf import settings +from drf_yasg.utils import swagger_serializer_method from rest_framework import serializers +from bkuser.apps.tenant.models import Tenant + logger = logging.getLogger(__name__) +class TenantDepartmentOutputSLZ(serializers.Serializer): + id = serializers.IntegerField(help_text="租户部门ID") + name = serializers.CharField(help_text="部门名称") + has_children = serializers.BooleanField(help_text="是否有子部门") + + +class TenantListOutputSLZ(serializers.Serializer): + id = serializers.CharField(help_text="租户ID") + name = serializers.CharField(help_text="租户名称") + logo = serializers.SerializerMethodField(help_text="租户 Logo") + departments = serializers.SerializerMethodField(help_text="租户下每个数据源的根组织") + + def get_logo(self, instance: Tenant) -> str: + return instance.logo or settings.DEFAULT_TENANT_LOGO + + @swagger_serializer_method(serializer_or_field=TenantDepartmentOutputSLZ(many=True)) + def get_departments(self, instance: Tenant): + departments = self.context["tenant_root_departments_map"].get(instance.id) or [] + return [item.model_dump(include={"id", "name", "has_children"}) for item in departments] + + class TenantDepartmentChildrenListOutputSLZ(serializers.Serializer): id = serializers.IntegerField(help_text="租户部门ID") name = serializers.CharField(help_text="部门名称") diff --git a/src/bk-user/bkuser/apis/web/organization/urls.py b/src/bk-user/bkuser/apis/web/organization/urls.py index bba98c32c..41d3449d7 100644 --- a/src/bk-user/bkuser/apis/web/organization/urls.py +++ b/src/bk-user/bkuser/apis/web/organization/urls.py @@ -13,6 +13,9 @@ from . import views urlpatterns = [ + # 租户 + path("tenants/", views.TenantListApi.as_view(), name="organization.tenant.list"), + path("tenants//", views.TenantRetrieveUpdateApi.as_view(), name="organization.tenant.retrieve_update"), path( "departments//children/", views.TenantDepartmentChildrenListApi.as_view(), diff --git a/src/bk-user/bkuser/apis/web/organization/views.py b/src/bk-user/bkuser/apis/web/organization/views.py index bacc99c6d..5c3f917a0 100644 --- a/src/bk-user/bkuser/apis/web/organization/views.py +++ b/src/bk-user/bkuser/apis/web/organization/views.py @@ -14,12 +14,86 @@ from rest_framework import generics, status from rest_framework.response import Response -from bkuser.apis.web.organization.serializers import TenantDepartmentChildrenListOutputSLZ -from bkuser.biz.tenant import TenantDepartmentHandler +from bkuser.apis.web.organization.serializers import TenantDepartmentChildrenListOutputSLZ, TenantListOutputSLZ +from bkuser.apis.web.tenant.serializers import TenantRetrieveOutputSLZ, TenantUpdateInputSLZ +from bkuser.apps.tenant.models import Tenant +from bkuser.biz.tenant import ( + TenantDepartmentHandler, + TenantEditableBaseInfo, + TenantFeatureFlag, + TenantHandler, +) +from bkuser.common.error_codes import error_codes +from bkuser.common.views import ExcludePatchAPIViewMixin logger = logging.getLogger(__name__) +class TenantListApi(generics.ListAPIView): + pagination_class = None + queryset = Tenant.objects.all() + serializer_class = TenantListOutputSLZ + + def _get_tenant_id(self) -> str: + return self.request.user.get_property("tenant_id") + + def get_serializer_context(self): + tenant_ids = list(self.queryset.values_list("id", flat=True)) + tenant_root_departments_map = TenantDepartmentHandler.get_tenant_root_department_map_by_tenant_id( + tenant_ids, self._get_tenant_id() + ) + return {"tenant_root_departments_map": tenant_root_departments_map} + + @swagger_auto_schema( + operation_description="租户列表", + responses={status.HTTP_200_OK: TenantListOutputSLZ(many=True)}, + ) + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + +class TenantRetrieveUpdateApi(ExcludePatchAPIViewMixin, generics.RetrieveUpdateAPIView): + queryset = Tenant.objects.all() + pagination_class = None + serializer_class = TenantRetrieveOutputSLZ + lookup_url_kwarg = "id" + + def _get_tenant_id(self) -> str: + return self.request.user.get_property("tenant_id") + + def get_serializer_context(self): + return {"current_tenant_id": self._get_tenant_id()} + + @swagger_auto_schema( + operation_description="单个租户详情", + responses={status.HTTP_200_OK: TenantRetrieveOutputSLZ()}, + ) + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + @swagger_auto_schema( + operation_description="更新租户", + request_body=TenantUpdateInputSLZ(), + responses={status.HTTP_200_OK: ""}, + ) + def put(self, request, *args, **kwargs): + slz = TenantUpdateInputSLZ(data=request.data) + slz.is_valid(raise_exception=True) + data = slz.validated_data + + instance = self.get_object() + # NOTE 因协同数据源而展示的租户,非当前租户, 无权限做更新操作 + if self._get_tenant_id() != instance.id: + raise error_codes.NO_PERMISSION + + should_updated_info = TenantEditableBaseInfo( + name=data["name"], logo=data["logo"] or "", feature_flags=TenantFeatureFlag(**data["feature_flags"]) + ) + + TenantHandler.update_with_managers(instance.id, should_updated_info, data["manager_ids"]) + return Response() + + class TenantDepartmentChildrenListApi(generics.ListAPIView): pagination_class = None serializer_class = TenantDepartmentChildrenListOutputSLZ @@ -33,4 +107,4 @@ def get(self, request, *args, **kwargs): # 拉取子部门信息列表 tenant_department_children = TenantDepartmentHandler.get_tenant_department_children_by_id(tenant_department_id) data = [item.model_dump(include={"id", "name", "has_children"}) for item in tenant_department_children] - return Response(self.get_serializer(data, many=True).data) + return Response(TenantDepartmentChildrenListOutputSLZ(data, many=True).data) diff --git a/src/bk-user/bkuser/apis/web/tenant/serializers.py b/src/bk-user/bkuser/apis/web/tenant/serializers.py index f9c43c36a..e6938ea41 100644 --- a/src/bk-user/bkuser/apis/web/tenant/serializers.py +++ b/src/bk-user/bkuser/apis/web/tenant/serializers.py @@ -17,7 +17,7 @@ from bkuser.apps.data_source.models import DataSourceUser from bkuser.apps.tenant.models import Tenant, TenantUser from bkuser.biz.data_source import DataSourceSimpleInfo -from bkuser.biz.tenant import TenantUserWithInheritedInfo +from bkuser.biz.tenant import TenantHandler, TenantUserWithInheritedInfo from bkuser.biz.validators import validate_tenant_id @@ -100,7 +100,7 @@ def get_data_sources(self, obj: Tenant) -> List[Dict]: class TenantUpdateInputSLZ(serializers.Serializer): name = serializers.CharField(help_text="租户名称") - logo = serializers.CharField(help_text="租户 Logo", required=False) + logo = serializers.CharField(help_text="租户 Logo", required=False, default=settings.DEFAULT_TENANT_LOGO) manager_ids = serializers.ListField(child=serializers.CharField(), help_text="租户用户 ID 列表", allow_empty=False) feature_flags = TenantFeatureFlagSLZ(help_text="租户特性集") @@ -125,16 +125,14 @@ class TenantRetrieveOutputSLZ(serializers.Serializer): @swagger_serializer_method(serializer_or_field=TenantRetrieveManagerOutputSLZ(many=True)) def get_managers(self, obj: Tenant) -> List[Dict]: - tenant_manager_map: Dict[str, List[TenantUserWithInheritedInfo]] = self.context["tenant_manager_map"] - managers = tenant_manager_map.get(obj.id) or [] + # 根据当前登录的租户用户,获取租户ID + # NOTE 因协同数据源而展示的租户,不返回管理员 + if obj.id != self.context["current_tenant_id"]: + return [] + managers = TenantHandler.retrieve_tenant_managers(obj.id) return [ - { - "id": i.id, - **i.data_source_user.model_dump( - include={"username", "full_name", "email", "phone", "phone_country_code"} - ), - } - for i in managers + {"id": manager.id, **manager.data_source_user.model_dump(include={"username", "full_name"})} + for manager in managers ] def get_logo(self, obj: Tenant) -> str: diff --git a/src/bk-user/bkuser/biz/data_source.py b/src/bk-user/bkuser/biz/data_source.py index f7ef53f27..90711c428 100644 --- a/src/bk-user/bkuser/biz/data_source.py +++ b/src/bk-user/bkuser/biz/data_source.py @@ -19,7 +19,7 @@ class DataSourceDepartmentInfoWithChildren(BaseModel): id: int name: str - children: List[int] + children_ids: List[int] class DataSourceSimpleInfo(BaseModel): @@ -45,34 +45,23 @@ def get_data_source_map_by_owner( return data - @staticmethod - def get_data_sources_by_tenant(tenant_ids: List[str]) -> Dict[str, List[int]]: - # 当前属于租户的数据源 - tenant_data_source_map: Dict = {} - data_sources = DataSource.objects.filter(owner_tenant_id__in=tenant_ids).values("id", "owner_tenant_id") - for item in data_sources: - tenant_id = item["owner_tenant_id"] - if tenant_id in tenant_data_source_map: - tenant_data_source_map[tenant_id].append(item["id"]) - else: - tenant_data_source_map[tenant_id] = [item["id"]] - # TODO 协同数据源获取 - return tenant_data_source_map - class DataSourceDepartmentHandler: @staticmethod - def get_department_info_by_id(department_ids: List[int]) -> Dict[int, DataSourceDepartmentInfoWithChildren]: + def get_department_info_map_by_id(department_ids: List[int]) -> Dict[int, DataSourceDepartmentInfoWithChildren]: """ 获取部门基础信息 """ departments = DataSourceDepartment.objects.filter(id__in=department_ids) departments_map: Dict = {} for item in departments: - children = DataSourceDepartmentRelation.objects.get(department=item).get_children() departments_map[item.id] = DataSourceDepartmentInfoWithChildren( id=item.id, name=item.name, - children=list(children.values_list("department_id", flat=True)), + children_ids=list( + DataSourceDepartmentRelation.objects.get(department=item) + .get_children() + .values_list("department_id", flat=True) + ), ) return departments_map diff --git a/src/bk-user/bkuser/biz/tenant.py b/src/bk-user/bkuser/biz/tenant.py index 1b63782b8..971b49c68 100644 --- a/src/bk-user/bkuser/biz/tenant.py +++ b/src/bk-user/bkuser/biz/tenant.py @@ -16,7 +16,7 @@ from bkuser.apps.data_source.models import DataSource, DataSourceDepartmentRelation, DataSourcePlugin, DataSourceUser from bkuser.apps.tenant.models import Tenant, TenantDepartment, TenantManager, TenantUser -from bkuser.biz.data_source import DataSourceDepartmentHandler, DataSourceHandler +from bkuser.biz.data_source import DataSourceDepartmentHandler, DataSourceHandler, DataSourceSimpleInfo from bkuser.utils.uuid import generate_uuid @@ -135,10 +135,8 @@ def retrieve_tenant_managers(tenant_id: str) -> List[TenantUserWithInheritedInfo """ 查询单个租户的租户管理员 """ - tenant_managers = TenantManager.objects.filter(tenant_id=tenant_id) - # 查询管理员对应的信息 - tenant_user_ids = [i.tenant_user_id for i in tenant_managers] - return TenantUserHandler.list_tenant_user_by_id(tenant_user_ids) + # 查询单个租户的管理员对应的信息 + return TenantHandler.get_tenant_manager_map([tenant_id]).get(tenant_id) or [] @staticmethod def create_with_managers(tenant_info: TenantBaseInfo, managers: List[TenantManagerWithoutID]) -> str: @@ -204,6 +202,19 @@ def update_with_managers(tenant_id: str, tenant_info: TenantEditableBaseInfo, ma batch_size=100, ) + @staticmethod + def get_data_source_ids_map_by_id(tenant_ids: List[str]) -> Dict[str, List[int]]: + # 当前属于租户的数据源 + tenant_data_source_map: Dict = {} + data_sources: Dict[str, List[DataSourceSimpleInfo]] = DataSourceHandler.get_data_source_map_by_owner( + tenant_ids + ) + for tenant_id, data_source_list in data_sources.items(): + data_source_ids: List = [data_source.id for data_source in data_source_list] + tenant_data_source_map[tenant_id] = data_source_ids + # TODO 协同数据源获取 + return tenant_data_source_map + class TenantDepartmentHandler: @staticmethod @@ -217,12 +228,12 @@ def convert_data_source_department_to_tenant_department( tenant_departments = TenantDepartment.objects.filter(tenant_id=tenant_id) # 获取数据源部门基础信息 - data_source_departments = DataSourceDepartmentHandler.get_department_info_by_id(data_source_department_ids) + data_source_departments = DataSourceDepartmentHandler.get_department_info_map_by_id(data_source_department_ids) # data_source_departments中包含了父子部门的ID,协同数据源需要查询绑定了该租户 department_ids = list(data_source_departments.keys()) for department in data_source_departments.values(): - department_ids += department.children + department_ids += department.children_ids # NOTE: 协同数据源,可能存在未授权全部子部门 # 提前拉取所有映射, 过滤绑定的租户部门 @@ -243,7 +254,9 @@ def convert_data_source_department_to_tenant_department( # 部门基础信息 data_source_department_info = data_source_departments[data_source_department_id] # 只要一个子部门被授权,都是存在子部门 - children_flag = [True for child in data_source_department_info.children if child in bound_departments_ids] + children_flag = [ + True for child in data_source_department_info.children_ids if child in bound_departments_ids + ] data.append( TenantDepartmentBaseInfo( id=tenant_department.id, @@ -254,10 +267,10 @@ def convert_data_source_department_to_tenant_department( return data @staticmethod - def get_tenant_root_departments_by_id( - tenant_ids: List[str], convert_tenant_id: str + def get_tenant_root_department_map_by_tenant_id( + tenant_ids: List[str], current_tenant_id: str ) -> Dict[str, List[TenantDepartmentBaseInfo]]: - data_source_map = DataSourceHandler.get_data_sources_by_tenant(tenant_ids) + data_source_map = TenantHandler.get_data_source_ids_map_by_id(tenant_ids) # 通过获取数据源的根节点 tenant_root_department_map: Dict = {} @@ -267,9 +280,9 @@ def get_tenant_root_departments_by_id( .filter(data_source_id__in=data_source_ids) .values_list("department_id", flat=True) ) - # 转换数据源部门为当前为 convert_tenant_id租户的租户部门 + # 转换数据源部门为当前为 current_tenant_id 租户的租户部门 tenant_root_department = TenantDepartmentHandler.convert_data_source_department_to_tenant_department( - tenant_id=convert_tenant_id, data_source_department_ids=list(root_department_ids) + tenant_id=current_tenant_id, data_source_department_ids=list(root_department_ids) ) tenant_root_department_map[tenant_id] = tenant_root_department return tenant_root_department_map