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

feat(tenant_organization): 租户列表展示/单个详情/编辑 #1169

Merged
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
25 changes: 25 additions & 0 deletions src/bk-user/bkuser/apis/web/organization/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="部门名称")
Expand Down
3 changes: 3 additions & 0 deletions src/bk-user/bkuser/apis/web/organization/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
from . import views

urlpatterns = [
# 租户
path("tenants/", views.TenantListApi.as_view(), name="organization.tenant.list"),
path("tenants/<str:id>/", views.TenantRetrieveUpdateApi.as_view(), name="organization.tenant.retrieve_update"),
path(
"departments/<int:id>/children/",
views.TenantDepartmentChildrenListApi.as_view(),
Expand Down
80 changes: 77 additions & 3 deletions src/bk-user/bkuser/apis/web/organization/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
neronkl marked this conversation as resolved.
Show resolved Hide resolved

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
Expand All @@ -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)
20 changes: 9 additions & 11 deletions src/bk-user/bkuser/apis/web/tenant/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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="租户特性集")

Expand All @@ -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:
Expand Down
25 changes: 7 additions & 18 deletions src/bk-user/bkuser/biz/data_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
class DataSourceDepartmentInfoWithChildren(BaseModel):
id: int
name: str
children: List[int]
children_ids: List[int]


class DataSourceSimpleInfo(BaseModel):
Expand All @@ -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
39 changes: 26 additions & 13 deletions src/bk-user/bkuser/biz/tenant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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: 协同数据源,可能存在未授权全部子部门
# 提前拉取所有映射, 过滤绑定的租户部门
Expand All @@ -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,
Expand All @@ -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 = {}
Expand All @@ -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)
)
neronkl marked this conversation as resolved.
Show resolved Hide resolved
tenant_root_department_map[tenant_id] = tenant_root_department
return tenant_root_department_map
Expand Down
Loading