diff --git a/src/bk-user/bkuser/apis/web/audit/__init__.py b/src/bk-user/bkuser/apis/web/audit/__init__.py new file mode 100644 index 000000000..95b0be489 --- /dev/null +++ b/src/bk-user/bkuser/apis/web/audit/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - 用户管理 (bk-user) available. +# Copyright (C) 2017 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. diff --git a/src/bk-user/bkuser/apis/web/audit/serializers.py b/src/bk-user/bkuser/apis/web/audit/serializers.py new file mode 100644 index 000000000..582f609a1 --- /dev/null +++ b/src/bk-user/bkuser/apis/web/audit/serializers.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - 用户管理 (bk-user) available. +# Copyright (C) 2017 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. + + +from rest_framework import serializers + +from bkuser.apps.audit.constants import ObjectTypeEnum, OperationEnum + + +class OperationAuditRecordListInputSerializer(serializers.Serializer): + operator = serializers.CharField(help_text="操作人", required=False, allow_blank=True) + operation = serializers.ChoiceField(help_text="操作行为", choices=OperationEnum.get_choices(), required=False) + object_type = serializers.ChoiceField( + help_text="操作对象类型", choices=ObjectTypeEnum.get_choices(), required=False + ) + object_name = serializers.CharField(help_text="操作对象名称", required=False, allow_blank=True) + created_at = serializers.DateTimeField(help_text="操作时间", required=False) + + +class OperationAuditRecordListOutputSerializer(serializers.Serializer): + operator = serializers.CharField(help_text="操作人", source="creator") + operation = serializers.SerializerMethodField(help_text="操作行为") + object_type = serializers.SerializerMethodField(help_text="操作对象类型") + object_name = serializers.CharField(help_text="操作对象名称", required=False) + created_at = serializers.DateTimeField(help_text="操作时间") + + def get_operation(self, obj): + # 从 operation_map 中提取 operation 对应的中文标识 + return self.context["operation_map"][obj.operation] + + def get_object_type(self, obj): + # 从 object_type_map 中提取 object_type 对应的中文标识 + return self.context["object_type_map"][obj.object_type] diff --git a/src/bk-user/bkuser/apis/web/audit/urls.py b/src/bk-user/bkuser/apis/web/audit/urls.py new file mode 100644 index 000000000..e31d080b7 --- /dev/null +++ b/src/bk-user/bkuser/apis/web/audit/urls.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - 用户管理 (bk-user) available. +# Copyright (C) 2017 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. + +from django.urls import path + +from bkuser.apis.web.audit import views + +urlpatterns = [ + # 操作审计列表 + path( + "", + views.AuditRecordListAPIView.as_view(), + name="audit.list", + ), +] diff --git a/src/bk-user/bkuser/apis/web/audit/views.py b/src/bk-user/bkuser/apis/web/audit/views.py new file mode 100644 index 000000000..9cc07b73a --- /dev/null +++ b/src/bk-user/bkuser/apis/web/audit/views.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - 用户管理 (bk-user) available. +# Copyright (C) 2017 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. + +from django.db.models import Q +from drf_yasg.utils import swagger_auto_schema +from rest_framework import generics, status +from rest_framework.permissions import IsAuthenticated + +from bkuser.apps.audit.constants import ObjectTypeEnum, OperationEnum +from bkuser.apps.audit.models import OperationAuditRecord +from bkuser.apps.permission.constants import PermAction +from bkuser.apps.permission.permissions import perm_class + +from .serializers import OperationAuditRecordListInputSerializer, OperationAuditRecordListOutputSerializer + + +class AuditRecordListAPIView(generics.ListAPIView): + """操作审计记录列表""" + + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] + + serializer_class = OperationAuditRecordListOutputSerializer + + def get_queryset(self): + slz = OperationAuditRecordListInputSerializer(data=self.request.query_params) + slz.is_valid(raise_exception=True) + params = slz.validated_data + + filters = Q() + if params.get("operator"): + filters &= Q(creator=params["operator"]) + if params.get("operation"): + filters &= Q(operation=params["operation"]) + if params.get("object_type"): + filters &= Q(object_type=params["object_type"]) + if params.get("created_at"): + start_time = params["created_at"].replace(microsecond=0) + end_time = params["created_at"].replace(microsecond=999999) + filters &= Q(created_at__range=(start_time, end_time)) + if params.get("object_name"): + filters &= Q(object_name__icontains=params["object_name"]) + + return OperationAuditRecord.objects.filter(filters) + + def get_serializer_context(self): + context = super().get_serializer_context() + context["operation_map"] = dict(OperationEnum.get_choices()) + context["object_type_map"] = dict(ObjectTypeEnum.get_choices()) + return context + + @swagger_auto_schema( + tags=["audit"], + operation_description="操作审计列表", + query_serializer=OperationAuditRecordListInputSerializer(), + responses={status.HTTP_200_OK: OperationAuditRecordListOutputSerializer(many=True)}, + ) + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) 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 cbd3646d4..fa9e8586c 100644 --- a/src/bk-user/bkuser/apis/web/data_source/views.py +++ b/src/bk-user/bkuser/apis/web/data_source/views.py @@ -52,7 +52,8 @@ ) from bkuser.apis.web.mixins import CurrentUserTenantMixin from bkuser.apps.audit.constants import ObjectTypeEnum, OperationEnum -from bkuser.apps.audit.recorder import add_audit_record +from bkuser.apps.audit.data_model import AuditObject +from bkuser.apps.audit.recorder import add_audit_record, batch_add_audit_records from bkuser.apps.data_source.constants import DataSourceTypeEnum from bkuser.apps.data_source.models import ( DataSource, @@ -80,6 +81,7 @@ from bkuser.idp_plugins.constants import BuiltinIdpPluginEnum from bkuser.plugins.base import get_default_plugin_cfg, get_plugin_cfg_schema_map, get_plugin_cls from bkuser.plugins.constants import DataSourcePluginEnum +from bkuser.utils.django import get_model_dict from .schema import get_data_source_plugin_cfg_json_schema @@ -193,11 +195,9 @@ def post(self, request, *args, **kwargs): operation=OperationEnum.CREATE_DATA_SOURCE, object_type=ObjectTypeEnum.DATA_SOURCE, object_id=ds.id, - extras={ - "plugin_config": ds.plugin_config, - "field_mapping": ds.field_mapping, - "sync_config": ds.sync_config, - }, + data_before={}, + data_after=get_model_dict(ds), + extras={}, ) return Response( @@ -253,11 +253,7 @@ def put(self, request, *args, **kwargs): data = slz.validated_data # 【审计】记录变更前数据 - data_before = { - "plugin_config": data_source.plugin_config, - "field_mapping": data_source.field_mapping, - "sync_config": data_source.sync_config, - } + data_before_data_source = get_model_dict(data_source) with transaction.atomic(): data_source.field_mapping = data["field_mapping"] @@ -274,7 +270,9 @@ def put(self, request, *args, **kwargs): operation=OperationEnum.MODIFY_DATA_SOURCE, object_type=ObjectTypeEnum.DATA_SOURCE, object_id=data_source.id, - extras={"data_before": data_before}, + data_before=data_before_data_source, + data_after=get_model_dict(data_source), + extras={}, ) return Response(status=status.HTTP_204_NO_CONTENT) @@ -308,14 +306,27 @@ def delete(self, request, *args, **kwargs): # 待删除的认证源 waiting_delete_idps = Idp.objects.filter(**idp_filters) - # 【审计】记录变更前数据,数据删除后便无法获取 - idps_before_delete = list( - waiting_delete_idps.values("id", "name", "status", "plugin_config", "data_source_match_rules") + # 记录 data_source 删除前数据 + data_source_audit_object = AuditObject( + id=data_source.id, + type=ObjectTypeEnum.DATA_SOURCE, + operation=OperationEnum.RESET_DATA_SOURCE, + data_before=get_model_dict(data_source), + data_after={}, + extras={}, ) - data_source_id = data_source.id - plugin_config = data_source.plugin_config - field_mapping = data_source.field_mapping - sync_config = data_source.sync_config + # 记录 idp 删除前数据 + idp_audit_objects = [ + AuditObject( + id=data_before_idp.id, + type=ObjectTypeEnum.IDP, + operation=OperationEnum.RESET_IDP, + data_before=get_model_dict(data_before_idp), + data_after={}, + extras={}, + ) + for data_before_idp in list(waiting_delete_idps) + ] with transaction.atomic(): # 删除认证源敏感信息 @@ -334,20 +345,12 @@ def delete(self, request, *args, **kwargs): # 删除数据源 & 关联资源数据 DataSourceHandler.delete_data_source_and_related_resources(data_source) + audit_objects = [data_source_audit_object] + idp_audit_objects # 审计记录 - add_audit_record( + batch_add_audit_records( operator=request.user.username, tenant_id=self.get_current_tenant_id(), - operation=OperationEnum.DELETE_DATA_SOURCE, - object_type=ObjectTypeEnum.DATA_SOURCE, - object_id=data_source_id, - extras={ - "is_delete_idp": is_delete_idp, - "plugin_config": plugin_config, - "field_mapping": field_mapping, - "sync_config": sync_config, - "idps_before_delete": idps_before_delete, - }, + objects=audit_objects, ) return Response(status=status.HTTP_204_NO_CONTENT) @@ -550,6 +553,8 @@ def post(self, request, *args, **kwargs): operation=OperationEnum.SYNC_DATA_SOURCE, object_type=ObjectTypeEnum.DATA_SOURCE, object_id=data_source.id, + data_before={}, + data_after={}, extras={"overwrite": options.overwrite, "incremental": options.incremental, "trigger": options.trigger}, ) @@ -603,6 +608,8 @@ def post(self, request, *args, **kwargs): operation=OperationEnum.SYNC_DATA_SOURCE, object_type=ObjectTypeEnum.DATA_SOURCE, object_id=data_source.id, + data_before={}, + data_after={}, extras={"overwrite": options.overwrite, "incremental": options.incremental, "trigger": options.trigger}, ) diff --git a/src/bk-user/bkuser/apis/web/organization/views/mixins.py b/src/bk-user/bkuser/apis/web/organization/views/mixins.py index 59e6bbb02..c7ca9ef02 100644 --- a/src/bk-user/bkuser/apis/web/organization/views/mixins.py +++ b/src/bk-user/bkuser/apis/web/organization/views/mixins.py @@ -14,11 +14,14 @@ # # We undertake not to change the open source license (MIT license) applicable # to the current version of the project delivered to anyone in the future. +from collections import defaultdict +from typing import Dict, List + from django.utils.translation import gettext_lazy as _ from bkuser.apis.web.mixins import CurrentUserTenantMixin from bkuser.apps.data_source.constants import DataSourceTypeEnum -from bkuser.apps.data_source.models import DataSource +from bkuser.apps.data_source.models import DataSource, DataSourceDepartmentUserRelation, DataSourceUserLeaderRelation from bkuser.common.error_codes import error_codes @@ -40,3 +43,37 @@ def get_current_tenant_local_real_data_source(self) -> DataSource: raise error_codes.DATA_SOURCE_NOT_EXIST.f(_("当前租户不存在本地实名用户数据源")) return real_data_source + + +class CurrentUserDepartmentRelationMixin: + """获取用户与部门之间的映射关系""" + + def get_user_department_map(self, data_source_user_ids: List[int]) -> Dict: + # 记录用户与部门之间的映射关系 + user_department_relations = DataSourceDepartmentUserRelation.objects.filter( + user_id__in=data_source_user_ids + ).values("department_id", "user_id") + user_department_map = defaultdict(list) + + # 将用户的所有部门存储在列表中 + for relation in user_department_relations: + user_department_map[relation["user_id"]].append(relation["department_id"]) + + return user_department_map + + +class CurrentUserLeaderRelationMixin: + """获取用户与上级之间的映射关系""" + + def get_user_leader_map(self, data_source_user_ids: List[int]) -> Dict: + # 记录用户与上级之间的映射关系 + user_leader_relations = DataSourceUserLeaderRelation.objects.filter(user_id__in=data_source_user_ids).values( + "leader_id", "user_id" + ) + user_leader_map = defaultdict(list) + + # 将用户的所有上级存储在列表中 + for relation in user_leader_relations: + user_leader_map[relation["user_id"]].append(relation["leader_id"]) + + return user_leader_map diff --git a/src/bk-user/bkuser/apis/web/organization/views/relations.py b/src/bk-user/bkuser/apis/web/organization/views/relations.py index 37e9152e3..de8fa8b58 100644 --- a/src/bk-user/bkuser/apis/web/organization/views/relations.py +++ b/src/bk-user/bkuser/apis/web/organization/views/relations.py @@ -29,14 +29,22 @@ TenantDeptUserRelationBatchPatchInputSLZ, TenantDeptUserRelationBatchUpdateInputSLZ, ) -from bkuser.apis.web.organization.views.mixins import CurrentUserTenantDataSourceMixin +from bkuser.apis.web.organization.views.mixins import ( + CurrentUserDepartmentRelationMixin, + CurrentUserTenantDataSourceMixin, +) +from bkuser.apps.audit.constants import ObjectTypeEnum, OperationEnum +from bkuser.apps.audit.data_model import AuditObject +from bkuser.apps.audit.recorder import batch_add_audit_records from bkuser.apps.data_source.models import DataSourceDepartmentUserRelation from bkuser.apps.permission.constants import PermAction from bkuser.apps.permission.permissions import perm_class from bkuser.apps.tenant.models import TenantDepartment, TenantUser -class TenantDeptUserRelationBatchCreateApi(CurrentUserTenantDataSourceMixin, generics.CreateAPIView): +class TenantDeptUserRelationBatchCreateApi( + CurrentUserTenantDataSourceMixin, CurrentUserDepartmentRelationMixin, generics.CreateAPIView +): """批量添加 / 拉取租户用户(添加部门 - 用户关系)""" permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] @@ -66,6 +74,9 @@ def post(self, request, *args, **kwargs): id__in=data["user_ids"], ).values_list("data_source_user_id", flat=True) + # 【审计】记录变更前用户-部门映射 + data_before_user_department_map = self.get_user_department_map(data_source_user_ids=data_source_user_ids) + # 复制操作:为数据源部门 & 用户添加关联边,但是不会影响存量的关联边 relations = [ DataSourceDepartmentUserRelation(user_id=user_id, department_id=dept_id, data_source=data_source) @@ -74,10 +85,39 @@ def post(self, request, *args, **kwargs): # 由于复制操作不会影响存量的关联边,所以需要忽略冲突,避免出现用户复选的情况 DataSourceDepartmentUserRelation.objects.bulk_create(relations, ignore_conflicts=True) + # 【审计】记录变更后用户-部门映射 + data_after_user_department_map = self.get_user_department_map(data_source_user_ids=data_source_user_ids) + + # 【审计】创建用户-部门审计对象 + audit_objects = [ + AuditObject( + id=tenant_user.data_source_user_id, + name=tenant_user.data_source_user.username, + type=ObjectTypeEnum.DATA_SOURCE_USER, + operation=OperationEnum.MODIFY_USER_DEPARTMENT_RELATIONS, + data_before={"department_ids": data_before_user_department_map[tenant_user.data_source_user_id]}, + data_after={"department_ids": data_after_user_department_map[tenant_user.data_source_user_id]}, + extras={"department_ids": list(data_source_dept_ids)}, + ) + for tenant_user in TenantUser.objects.filter( + tenant_id=cur_tenant_id, + id__in=data["user_ids"], + ).select_related("data_source_user") + ] + + # 【审计】批量添加审计记录 + batch_add_audit_records( + operator=request.user.username, + tenant_id=cur_tenant_id, + objects=audit_objects, + ) + return Response(status=status.HTTP_204_NO_CONTENT) -class TenantDeptUserRelationBatchUpdateApi(CurrentUserTenantDataSourceMixin, generics.UpdateAPIView): +class TenantDeptUserRelationBatchUpdateApi( + CurrentUserTenantDataSourceMixin, CurrentUserDepartmentRelationMixin, generics.UpdateAPIView +): """批量移动租户用户(更新部门 - 用户关系)""" permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] @@ -107,6 +147,9 @@ def put(self, request, *args, **kwargs): id__in=data["user_ids"], ).values_list("data_source_user_id", flat=True) + # 【审计】记录变更前用户-部门映射 + data_before_user_department_map = self.get_user_department_map(data_source_user_ids=data_source_user_ids) + # 移动操作:为数据源部门 & 用户添加关联边,但是会删除这批用户所有的存量关联边 with transaction.atomic(): # 先删除 @@ -118,6 +161,33 @@ def put(self, request, *args, **kwargs): ] DataSourceDepartmentUserRelation.objects.bulk_create(relations) + # 【审计】记录变更后用户-部门映射 + data_after_user_department_map = self.get_user_department_map(data_source_user_ids=data_source_user_ids) + + # 【审计】创建用户-部门审计对象 + audit_objects = [ + AuditObject( + id=tenant_user.data_source_user_id, + name=tenant_user.data_source_user.username, + type=ObjectTypeEnum.DATA_SOURCE_USER, + operation=OperationEnum.MODIFY_USER_DEPARTMENT_RELATIONS, + data_before={"department_ids": data_before_user_department_map[tenant_user.data_source_user_id]}, + data_after={"department_ids": data_after_user_department_map[tenant_user.data_source_user_id]}, + extras={"department_ids": list(data_source_dept_ids)}, + ) + for tenant_user in TenantUser.objects.filter( + tenant_id=cur_tenant_id, + id__in=data["user_ids"], + ).select_related("data_source_user") + ] + + # 【审计】批量添加审计记录 + batch_add_audit_records( + operator=request.user.username, + tenant_id=cur_tenant_id, + objects=audit_objects, + ) + return Response(status=status.HTTP_204_NO_CONTENT) @swagger_auto_schema( @@ -147,6 +217,9 @@ def patch(self, request, *args, **kwargs): id__in=data["user_ids"], ).values_list("data_source_user_id", flat=True) + # 【审计】记录变更前用户-部门映射 + data_before_user_department_map = self.get_user_department_map(data_source_user_ids=data_source_user_ids) + # 移动操作:为数据源部门 & 用户添加关联边,但是会删除这批用户在当前部门的存量关联边 with transaction.atomic(): # 先删除(仅限于指定部门) @@ -160,10 +233,39 @@ def patch(self, request, *args, **kwargs): ] DataSourceDepartmentUserRelation.objects.bulk_create(relations, ignore_conflicts=True) + # 【审计】记录变更后用户-部门映射 + data_after_user_department_map = self.get_user_department_map(data_source_user_ids=data_source_user_ids) + + # 【审计】创建用户-部门审计对象 + audit_objects = [ + AuditObject( + id=tenant_user.data_source_user_id, + name=tenant_user.data_source_user.username, + type=ObjectTypeEnum.DATA_SOURCE_USER, + operation=OperationEnum.MODIFY_USER_DEPARTMENT_RELATIONS, + data_before={"department_ids": data_before_user_department_map[tenant_user.data_source_user_id]}, + data_after={"department_ids": data_after_user_department_map[tenant_user.data_source_user_id]}, + extras={}, + ) + for tenant_user in TenantUser.objects.filter( + tenant_id=cur_tenant_id, + id__in=data["user_ids"], + ).select_related("data_source_user") + ] + + # 【审计】批量添加审计记录 + batch_add_audit_records( + operator=request.user.username, + tenant_id=cur_tenant_id, + objects=audit_objects, + ) + return Response(status=status.HTTP_204_NO_CONTENT) -class TenantDeptUserRelationBatchDeleteApi(CurrentUserTenantDataSourceMixin, generics.DestroyAPIView): +class TenantDeptUserRelationBatchDeleteApi( + CurrentUserTenantDataSourceMixin, CurrentUserDepartmentRelationMixin, generics.DestroyAPIView +): """批量删除指定部门 & 用户的部门 - 用户关系""" permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] @@ -191,8 +293,38 @@ def delete(self, request, *args, **kwargs): id__in=data["user_ids"], ).values_list("data_source_user_id", flat=True) + # 【审计】记录变更前用户-部门映射 + data_before_user_department_map = self.get_user_department_map(data_source_user_ids=data_source_user_ids) + DataSourceDepartmentUserRelation.objects.filter( user_id__in=data_source_user_ids, department=source_data_source_dept ).delete() + # 【审计】记录变更后用户-部门映射 + data_after_user_department_map = self.get_user_department_map(data_source_user_ids=data_source_user_ids) + + # 【审计】创建用户-部门审计对象 + audit_objects = [ + AuditObject( + id=tenant_user.data_source_user_id, + name=tenant_user.data_source_user.username, + type=ObjectTypeEnum.DATA_SOURCE_USER, + operation=OperationEnum.MODIFY_USER_DEPARTMENT_RELATIONS, + data_before={"department_ids": data_before_user_department_map[tenant_user.data_source_user_id]}, + data_after={"department_ids": data_after_user_department_map[tenant_user.data_source_user_id]}, + extras={"department_id": source_data_source_dept.id}, + ) + for tenant_user in TenantUser.objects.filter( + tenant_id=cur_tenant_id, + id__in=data["user_ids"], + ).select_related("data_source_user") + ] + + # 【审计】批量添加审计记录 + batch_add_audit_records( + operator=request.user.username, + tenant_id=cur_tenant_id, + objects=audit_objects, + ) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/src/bk-user/bkuser/apis/web/organization/views/users.py b/src/bk-user/bkuser/apis/web/organization/views/users.py index 09d33fbf6..dd635585b 100644 --- a/src/bk-user/bkuser/apis/web/organization/views/users.py +++ b/src/bk-user/bkuser/apis/web/organization/views/users.py @@ -57,7 +57,11 @@ TenantUserStatusUpdateOutputSLZ, TenantUserUpdateInputSLZ, ) -from bkuser.apis.web.organization.views.mixins import CurrentUserTenantDataSourceMixin +from bkuser.apis.web.organization.views.mixins import ( + CurrentUserDepartmentRelationMixin, + CurrentUserLeaderRelationMixin, + CurrentUserTenantDataSourceMixin, +) from bkuser.apps.data_source.constants import DataSourceTypeEnum from bkuser.apps.data_source.models import ( DataSource, @@ -79,6 +83,7 @@ TenantUserValidityPeriodConfig, ) from bkuser.apps.tenant.utils import TenantUserIDGenerator, is_username_frozen +from bkuser.biz.auditor import TenantUserCreateAuditor, TenantUserUpdateDestroyAuditor from bkuser.biz.organization import DataSourceUserHandler from bkuser.common.constants import PERMANENT_TIME from bkuser.common.error_codes import error_codes @@ -496,6 +501,10 @@ def put(self, request, *args, **kwargs): data_source=data_source, id__in=data["leader_ids"] ).values_list("data_source_user_id", flat=True) + # 【审计】记录变更前的用户相关信息(数据源用户、部门、上级、租户用户) + auditor = TenantUserUpdateDestroyAuditor(request.user.username, cur_tenant_id) + auditor.pre_record_data_before(tenant_user, data_source_user) + with transaction.atomic(): data_source_user.username = data["username"] data_source_user.full_name = data["full_name"] @@ -521,11 +530,16 @@ def put(self, request, *args, **kwargs): tenant_user.save(update_fields=["account_expired_at", "status", "updater", "updated_at"]) + # 【审计】保存记录至数据库 + auditor.record(tenant_user, data_source_user) + auditor.save_audit_records() + return Response(status=status.HTTP_204_NO_CONTENT) def delete(self, request, *args, **kwargs): tenant_user = self.get_object() data_source = tenant_user.data_source + cur_tenant_id = self.get_current_tenant_id() if not (data_source.is_local and data_source.is_real_type): raise error_codes.TENANT_USER_DELETE_FAILED.f(_("仅本地实名数据源支持删除用户")) @@ -534,6 +548,11 @@ def delete(self, request, *args, **kwargs): raise error_codes.TENANT_USER_DELETE_FAILED.f(_("仅可删除非协同产生的租户用户")) data_source_user = tenant_user.data_source_user + + # 【审计】记录待删除的用户相关信息(数据源用户、部门、上级、租户用户(包括协同租户用户)) + auditor = TenantUserUpdateDestroyAuditor(request.user.username, cur_tenant_id, True) + auditor.pre_record_data_before(tenant_user, data_source_user) + with transaction.atomic(): # 删除用户意味着租户用户 & 数据源用户都删除,前面检查过权限, # 因此这里所有协同产生的租户用户也需要删除(不等同步,立即生效) @@ -543,6 +562,10 @@ def delete(self, request, *args, **kwargs): DataSourceUserLeaderRelation.objects.filter(leader=data_source_user).delete() data_source_user.delete() + # 【审计】保存记录至数据库 + auditor.record(tenant_user, data_source_user) + auditor.save_audit_records() + return Response(status=status.HTTP_204_NO_CONTENT) @@ -807,6 +830,15 @@ def post(self, request, *args, **kwargs): # 批量创建租户用户(含协同) self._bulk_create_tenant_users(cur_tenant_id, tenant_dept, data_source, data_source_users) + # 【审计】重新查询租户用户和协同租户用户 + data_after_tenant_users = TenantUser.objects.filter( + data_source=data_source, data_source_user__in=data_source_users + ) + + # 【审计】记录创建的用户相关信息(数据源用户、用户部门、租户用户(包括协同租户用户)) + auditor = TenantUserCreateAuditor(request.user.username, cur_tenant_id) + auditor.batch_record(data_after_tenant_users) + # 对新增的用户进行账密信息初始化 & 发送密码通知 initialize_identity_info_and_send_notification.delay(data_source.id) return Response(status=status.HTTP_204_NO_CONTENT) @@ -892,7 +924,12 @@ def post(self, request, *args, **kwargs): ) -class TenantUserBatchDeleteApi(CurrentUserTenantDataSourceMixin, generics.DestroyAPIView): +class TenantUserBatchDeleteApi( + CurrentUserTenantDataSourceMixin, + CurrentUserDepartmentRelationMixin, + CurrentUserLeaderRelationMixin, + generics.DestroyAPIView, +): """批量删除租户用户""" permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] @@ -922,6 +959,13 @@ def delete(self, request, *args, **kwargs): ).values_list("data_source_user_id", flat=True) ) + # 【审计】记录待删除的租户用户和数据源用户 + data_before_tenant_users = list(TenantUser.objects.filter(data_source_user_id__in=data_source_user_ids)) + + # 【审计】记录待删除的用户相关信息(数据源用户、部门、上级、租户用户(包括协同租户用户)) + auditor = TenantUserUpdateDestroyAuditor(request.user.username, cur_tenant_id, True) + auditor.batch_pre_record_data_before(data_before_tenant_users) + with transaction.atomic(): # 删除用户意味着租户用户 & 数据源用户都删除,前面检查过权限, # 因此这里所有协同产生的租户用户也需要删除(不等同步,立即生效) @@ -935,6 +979,10 @@ def delete(self, request, *args, **kwargs): # 最后才是批量回收数据源用户 DataSourceUser.objects.filter(id__in=data_source_user_ids).delete() + # 【审计】保存记录至数据库 + auditor.batch_record(data_before_tenant_users) + auditor.save_audit_records() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/src/bk-user/bkuser/apps/audit/constants.py b/src/bk-user/bkuser/apps/audit/constants.py index 92e11a970..2312e72b4 100644 --- a/src/bk-user/bkuser/apps/audit/constants.py +++ b/src/bk-user/bkuser/apps/audit/constants.py @@ -24,7 +24,8 @@ class ObjectTypeEnum(str, StructuredEnum): DATA_SOURCE = EnumField("data_source", label=_("数据源")) IDP = EnumField("idp", label=_("认证源")) - USER = EnumField("user", label=_("用户")) + DATA_SOURCE_USER = EnumField("data_source_user", label=_("数据源用户")) + TENANT_USER = EnumField("tenant_user", label=_("租户用户")) DEPARTMENT = EnumField("department", label=_("部门")) TENANT = EnumField("tenant", label=_("租户")) VIRTUAL_USER = EnumField("virtual_user", label=_("虚拟用户")) @@ -36,20 +37,33 @@ class OperationEnum(str, StructuredEnum): # 数据源 CREATE_DATA_SOURCE = EnumField("create_data_source", label=_("创建数据源")) MODIFY_DATA_SOURCE = EnumField("modify_data_source", label=_("修改数据源")) - DELETE_DATA_SOURCE = EnumField("delete_data_source", label=_("删除数据源")) + RESET_DATA_SOURCE = EnumField("reset_data_source", label=_("重置数据源")) SYNC_DATA_SOURCE = EnumField("sync_data_source", label=_("同步数据源")) # 认证源 CREATE_IDP = EnumField("create_idp", label=_("创建认证源")) MODIFY_IDP = EnumField("modify_idp", label=_("修改认证源")) MODIFY_IDP_STATUS = EnumField("modify_idp_status", label=_("修改认证源状态")) + RESET_IDP = EnumField("reset_idp", label=_("重置认证源")) # 用户 - CREATE_USER = EnumField("create_user", label=_("创建用户")) - MODIFY_USER = EnumField("modify_user", label=_("修改用户信息")) - DELETE_USER = EnumField("delete_user", label=_("删除用户")) - MODIFY_USER_DEPARTMENT_RELATIONS = EnumField("modify_user_department_relations", label=_("修改用户所属部门")) + CREATE_DATA_SOURCE_USER = EnumField("create_data_source_user", label=_("创建数据源用户")) + CREATE_TENANT_USER = EnumField("create_tenant_user", label=_("创建租户用户")) + CREATE_USER_LEADER_RELATIONS = EnumField("create_user_leader_relations", label=_("创建用户-上级关系")) + CREATE_USER_DEPARTMENT_RELATIONS = EnumField("create_user_department_relations", label=_("创建用户-部门关系")) + CREATE_COLLABORATION_TENANT_USER = EnumField("create_collaboration_tenant_user", label=_("创建协同租户用户")) + + MODIFY_DATA_SOURCE_USER = EnumField("modify_data_source_user", label=_("修改数据源用户")) + MODIFY_TENANT_USER = EnumField("modify_tenant_user", label=_("修改租户用户")) + MODIFY_USER_LEADER_RELATIONS = EnumField("modify_user_leader_relations", label=_("修改用户-上级关系")) + MODIFY_USER_DEPARTMENT_RELATIONS = EnumField("modify_user_department_relations", label=_("修改用户-部门关系")) + + DELETE_DATA_SOURCE_USER = EnumField("delete_data_source_user", label=_("删除数据源用户")) + DELETE_TENANT_USER = EnumField("delete_tenant_user", label=_("删除租户用户")) + DELETE_USER_LEADER_RELATIONS = EnumField("delete_user_leader_relations", label=_("删除用户-上级关系")) + DELETE_USER_DEPARTMENT_RELATIONS = EnumField("delete_user_department_relations", label=_("删除用户-部门关系")) + DELETE_COLLABORATION_TENANT_USER = EnumField("delete_collaboration_tenant_user", label=_("删除协同租户用户")) + MODIFY_USER_STATUS = EnumField("modify_user_status", label=_("修改用户状态")) MODIFY_USER_ACCOUNT_EXPIRED_AT = EnumField("modify_user_account_expired_at", label=_("修改用户账号过期时间")) - MODIFY_USER_LEADERS = EnumField("modify_user_leaders", label=_("修改用户上级")) MODIFY_USER_PASSWORD = EnumField("modify_user_password", label=_("重置用户密码")) MODIFY_USER_EMAIL = EnumField("modify_user_email", label=_("修改用户邮箱")) MODIFY_USER_PHONE = EnumField("modify_user_phone", label=_("修改用户电话号码")) diff --git a/src/bk-user/bkuser/apps/audit/data_model.py b/src/bk-user/bkuser/apps/audit/data_model.py index bcdc4cf6c..7f2f7abab 100644 --- a/src/bk-user/bkuser/apps/audit/data_model.py +++ b/src/bk-user/bkuser/apps/audit/data_model.py @@ -25,5 +25,15 @@ class AuditObject(BaseModel): # 操作对象 ID id: str | int - # 操作对象额外信息 + # 操作对象类型 + type: str + # 操作对象名称 + name: str = "" + # 操作行为 + operation: str + # 操作前数据 + data_before: Dict = Field(default_factory=dict) + # 操作后数据 + data_after: Dict = Field(default_factory=dict) + # 额外信息 extras: Dict = Field(default_factory=dict) diff --git a/src/bk-user/bkuser/apps/audit/migrations/0001_initial.py b/src/bk-user/bkuser/apps/audit/migrations/0001_initial.py index 352238f64..9e75c782c 100644 --- a/src/bk-user/bkuser/apps/audit/migrations/0001_initial.py +++ b/src/bk-user/bkuser/apps/audit/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.25 on 2024-10-31 03:46 +# Generated by Django 3.2.25 on 2024-11-13 06:50 import bkuser.utils.uuid from django.db import migrations, models @@ -25,6 +25,9 @@ class Migration(migrations.Migration): ('operation', models.CharField(max_length=64, verbose_name='操作行为')), ('object_type', models.CharField(max_length=32, verbose_name='操作对象类型')), ('object_id', models.CharField(max_length=128, verbose_name='操作对象 ID')), + ('object_name', models.CharField(default='', max_length=128, verbose_name='操作对象名称')), + ('data_before', models.JSONField(default=dict, verbose_name='操作前数据')), + ('data_after', models.JSONField(default=dict, verbose_name='操作后数据')), ('extras', models.JSONField(default=dict, verbose_name='额外信息')), ], options={ diff --git a/src/bk-user/bkuser/apps/audit/models.py b/src/bk-user/bkuser/apps/audit/models.py index cf09d2a75..39d41bd7f 100644 --- a/src/bk-user/bkuser/apps/audit/models.py +++ b/src/bk-user/bkuser/apps/audit/models.py @@ -34,7 +34,10 @@ class OperationAuditRecord(AuditedModel): operation = models.CharField("操作行为", max_length=64) object_type = models.CharField("操作对象类型", max_length=32) object_id = models.CharField("操作对象 ID", max_length=128) - # 与操作对象相关的额外信息,有助于问题溯源 + object_name = models.CharField("操作对象名称", max_length=128, default="") + # 记录操作前后的数据,有助于问题溯源 + data_before = models.JSONField("操作前数据", default=dict) + data_after = models.JSONField("操作后数据", default=dict) extras = models.JSONField("额外信息", default=dict) class Meta: diff --git a/src/bk-user/bkuser/apps/audit/recorder.py b/src/bk-user/bkuser/apps/audit/recorder.py index 178610863..57e6b793a 100644 --- a/src/bk-user/bkuser/apps/audit/recorder.py +++ b/src/bk-user/bkuser/apps/audit/recorder.py @@ -29,8 +29,11 @@ def add_audit_record( tenant_id: str, operation: OperationEnum, object_type: ObjectTypeEnum, + data_before: Dict, + data_after: Dict, object_id: str | int, - extras: Dict | None = None, + extras: Dict, + object_name: str = "", ): """ 添加操作审计记录 @@ -40,23 +43,30 @@ def add_audit_record( :param operation: 操作行为 :param object_type: 操作对象类型 :param object_id: 操作对象 ID - :param extras: 额外信息 + :param data_before: 修改前数据 + :param data_after: 修改前数据 + :param extras: 额外相关数据 + :param object_name: 操作对象名称 """ - OperationAuditRecord.objects.create( - creator=operator, - tenant_id=tenant_id, - operation=operation, - object_type=object_type, - object_id=str(object_id), - extras=extras or {}, - ) + + # 若有数据变更,则添加记录 + if data_before != data_after or extras != {}: + OperationAuditRecord.objects.create( + creator=operator, + tenant_id=tenant_id, + operation=operation, + object_type=object_type, + object_id=str(object_id), + object_name=object_name, + data_before=sort_dict_values(data_before), + data_after=sort_dict_values(data_after), + extras=extras, + ) def batch_add_audit_records( operator: str, tenant_id: str, - operation: OperationEnum, - object_type: ObjectTypeEnum, objects: List[AuditObject], ): """ @@ -64,9 +74,7 @@ def batch_add_audit_records( :param operator: 操作者 :param tenant_id: 租户 ID - :param operation: 操作类型 - :param object_type: 对象类型 - :param objects: AuditObject(包含操作对象 ID 和额外信息)对象列表 + :param objects: AuditObject(包含操作对象相关信息)对象列表 """ # 生成事件 ID event_id = generate_uuid() @@ -76,12 +84,31 @@ def batch_add_audit_records( creator=operator, event_id=event_id, tenant_id=tenant_id, - operation=operation, - object_type=object_type, + operation=obj.operation, + object_type=obj.type, object_id=str(obj.id), + object_name=obj.name, + data_before=sort_dict_values(obj.data_before), + data_after=sort_dict_values(obj.data_after), extras=obj.extras, ) for obj in objects + # 若有数据变更,则添加记录 + if obj.data_before != obj.data_after or obj.extras != {} ] OperationAuditRecord.objects.bulk_create(records, batch_size=100) + + +def sort_dict_values(ordinary_dict: Dict) -> Dict: + """ + 对字典的值为列表的项进行排序,返回排序后的字典 + + :param ordinary_dict: 原始字典 + :return: 排序后的字典 + """ + return { + # 仅对值为列表的项进行排序 + k: sorted(v) if isinstance(v, list) else v + for k, v in ordinary_dict.items() + } diff --git a/src/bk-user/bkuser/biz/auditor.py b/src/bk-user/bkuser/biz/auditor.py new file mode 100644 index 000000000..ade571834 --- /dev/null +++ b/src/bk-user/bkuser/biz/auditor.py @@ -0,0 +1,251 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - 用户管理 (bk-user) available. +# Copyright (C) 2017 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. +from typing import Dict, List + +from bkuser.apps.audit.constants import ObjectTypeEnum, OperationEnum +from bkuser.apps.audit.data_model import AuditObject +from bkuser.apps.audit.recorder import batch_add_audit_records +from bkuser.apps.data_source.models import ( + DataSourceDepartmentUserRelation, + DataSourceUser, + DataSourceUserLeaderRelation, +) +from bkuser.apps.tenant.models import TenantUser +from bkuser.utils.django import get_model_dict + + +class TenantUserUpdateDestroyAuditor: + """用于记录租户用户修改与删除的审计""" + + def __init__(self, operator: str, tenant_id: str, is_delete: bool = False): + self.operator = operator + self.tenant_id = tenant_id + self.is_delete = is_delete + + self.data_befores: Dict[str, Dict] = {} + self.audit_objects: List[AuditObject] = [] + + def pre_record_data_before( + self, + tenant_user: TenantUser, + data_source_user: DataSourceUser, + ): + """记录变更前的相关数据记录""" + + # 为每个用户的审计数据创建唯一的键 + tenant_user_id = tenant_user.id + + # 初始化对应 tenant_user 的审计数据 + self.data_befores[tenant_user_id] = { + "tenant_user": get_model_dict(tenant_user), + "data_source_user": get_model_dict(data_source_user), + "collaboration_tenant_users": {}, + "department_ids": [], + "leader_ids": [], + } + + # 记录修改前的协同租户用户 + if self.is_delete: + # 获取与 data_source_user_id 相关的所有 collab_user,排除当前的 tenant_user + collab_users = TenantUser.objects.filter(data_source_user_id=data_source_user.id).exclude( + id=tenant_user.id + ) + + # 构造每一个 collab_user 映射 + self.data_befores[tenant_user_id]["collaboration_tenant_users"] = { + collab_user.id: get_model_dict(collab_user) for collab_user in collab_users + } + + # 记录修改前的用户部门 + self.data_befores[tenant_user_id]["department_ids"] = list( + DataSourceDepartmentUserRelation.objects.filter( + user=data_source_user, + ).values_list("department_id", flat=True) + ) + + # 记录修改前的用户上级 + self.data_befores[tenant_user_id]["leader_ids"] = list( + DataSourceUserLeaderRelation.objects.filter(user=data_source_user).values_list("leader_id", flat=True) + ) + + def batch_pre_record_data_before( + self, + tenant_users: List[TenantUser], + ): + """批量记录变更前的相关数据记录""" + + for tenant_user in tenant_users: + data_source_user = tenant_user.data_source_user + self.pre_record_data_before(tenant_user, data_source_user) + + def get_current_operation(self, operation: OperationEnum) -> OperationEnum: + """根据操作行为返回对应的删除或修改操作""" + + operation_map = { + OperationEnum.MODIFY_DATA_SOURCE_USER: OperationEnum.DELETE_DATA_SOURCE_USER, + OperationEnum.MODIFY_USER_DEPARTMENT_RELATIONS: OperationEnum.DELETE_USER_DEPARTMENT_RELATIONS, + OperationEnum.MODIFY_USER_LEADER_RELATIONS: OperationEnum.DELETE_USER_LEADER_RELATIONS, + OperationEnum.MODIFY_TENANT_USER: OperationEnum.DELETE_TENANT_USER, + } + + return operation_map[operation] if self.is_delete else operation + + def record(self, tenant_user: TenantUser, data_source_user: DataSourceUser): + """组装相关数据,并调用 apps.audit 模块里的方法进行记录""" + + ds_user_id = ( + self.data_befores[tenant_user.id]["data_source_user"]["id"] if self.is_delete else data_source_user.id + ) + ds_user_name = ( + self.data_befores[tenant_user.id]["data_source_user"]["username"] + if self.is_delete + else data_source_user.username + ) + + ds_user_object = {"id": ds_user_id, "name": ds_user_name, "type": ObjectTypeEnum.DATA_SOURCE_USER} + + self.audit_objects = [ + # 数据源用户本身信息 + AuditObject( + **ds_user_object, + operation=self.get_current_operation(OperationEnum.MODIFY_DATA_SOURCE_USER), + data_before=self.data_befores[tenant_user.id]["data_source_user"], + data_after=get_model_dict(data_source_user) if not self.is_delete else {}, + ), + # 数据源用户的部门 + AuditObject( + **ds_user_object, + operation=self.get_current_operation(OperationEnum.MODIFY_USER_DEPARTMENT_RELATIONS), + data_before={"department_ids": self.data_befores[tenant_user.id]["department_ids"]}, + data_after={ + "department_ids": list( + DataSourceDepartmentUserRelation.objects.filter( + user=data_source_user, + ).values_list("department_id", flat=True) + ) + if not self.is_delete + else [] + }, + ), + # 数据源用户的 Leader + AuditObject( + **ds_user_object, + operation=self.get_current_operation(OperationEnum.MODIFY_USER_LEADER_RELATIONS), + data_before={"leader_ids": self.data_befores[tenant_user.id]["leader_ids"]}, + data_after={ + "leader_ids": list( + DataSourceUserLeaderRelation.objects.filter(user=data_source_user).values_list( + "leader_id", flat=True + ) + ) + if not self.is_delete + else [] + }, + ), + # 租户用户 + AuditObject( + id=tenant_user.id, + type=ObjectTypeEnum.TENANT_USER, + operation=self.get_current_operation(OperationEnum.MODIFY_TENANT_USER), + data_before=self.data_befores[tenant_user.id]["tenant_user"], + data_after=get_model_dict(tenant_user) if not self.is_delete else {}, + ), + ] + + # 若为删除操作,则需记录删除前的协同租户用户 + if self.is_delete: + self.audit_objects.extend( + [ + AuditObject( + id=user_id, + type=ObjectTypeEnum.TENANT_USER, + operation=OperationEnum.DELETE_COLLABORATION_TENANT_USER, + data_before=user_data, + data_after={}, + ) + for user_id, user_data in self.data_befores[tenant_user.id]["collaboration_tenant_users"].items() + ] + ) + + def batch_record(self, tenant_users: List[TenantUser]): + """批量记录""" + + for tenant_user in tenant_users: + self.record(tenant_user, tenant_user.data_source_user) + + # 由于不确定操作是否为批量,故将底层存储数据库的方法抽象,需单独调用 + def save_audit_records(self): + batch_add_audit_records(self.operator, self.tenant_id, self.audit_objects) + + +class TenantUserCreateAuditor: + """用于记录租户用户创建的审计""" + + def __init__(self, operator: str, tenant_id: str): + self.operator = operator + self.tenant_id = tenant_id + self.audit_objects: List[AuditObject] = [] + + def record(self, tenant_user: TenantUser, data_source_user: DataSourceUser): + """组装相关数据,并调用 apps.audit 模块里的方法进行记录""" + ds_user_object = { + "id": data_source_user.id, + "name": data_source_user.username, + "type": ObjectTypeEnum.DATA_SOURCE_USER, + } + + self.audit_objects.extend( + [ + # 数据源用户本身信息 + AuditObject( + **ds_user_object, + operation=OperationEnum.CREATE_DATA_SOURCE_USER, + data_before={}, + data_after=get_model_dict(data_source_user), + ), + # 数据源用户的部门 + AuditObject( + **ds_user_object, + operation=OperationEnum.CREATE_USER_DEPARTMENT_RELATIONS, + data_before={}, + data_after={ + "department_ids": list( + DataSourceDepartmentUserRelation.objects.filter( + user=data_source_user, + ).values_list("department_id", flat=True) + ) + }, + ), + # 租户用户(包含协同租户用户) + AuditObject( + id=tenant_user.id, + type=ObjectTypeEnum.TENANT_USER, + operation=OperationEnum.CREATE_COLLABORATION_TENANT_USER + if tenant_user.tenant_id != self.tenant_id + else OperationEnum.CREATE_TENANT_USER, + data_before={}, + data_after=get_model_dict(tenant_user), + ), + ] + ) + + def batch_record(self, tenant_users: List[TenantUser]): + """批量记录""" + + for tenant_user in tenant_users: + self.record(tenant_user, tenant_user.data_source_user) + batch_add_audit_records(self.operator, self.tenant_id, self.audit_objects) diff --git a/src/bk-user/bkuser/utils/django.py b/src/bk-user/bkuser/utils/django.py new file mode 100644 index 000000000..2d439a8a7 --- /dev/null +++ b/src/bk-user/bkuser/utils/django.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - 用户管理 (bk-user) available. +# Copyright (C) 2017 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. + +import json +from typing import Any, Dict + +from django.core.serializers.json import DjangoJSONEncoder +from django.forms import model_to_dict + + +def get_model_dict(obj) -> Dict[str, Any]: + # 获取模型的所有字段名称 + fields = [field.name for field in obj._meta.fields] + # 将模型对象转换为字典 + model_dict = model_to_dict(obj, fields=fields) + # 使用 DjangoJSONEncoder 将字典转换为 JSON 字符串,然后再解析回字典 + return json.loads(json.dumps(model_dict, cls=DjangoJSONEncoder)) diff --git a/src/bk-user/locale/en/LC_MESSAGES/django.po b/src/bk-user/locale/en/LC_MESSAGES/django.po index bfd8b3dbf..a3d64f486 100644 --- a/src/bk-user/locale/en/LC_MESSAGES/django.po +++ b/src/bk-user/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-30 16:12+0800\n" +"POT-Creation-Date: 2024-11-11 17:37+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -178,45 +178,45 @@ msgstr "The file to be imported must not exceed {} M in size" msgid "出于安全考虑,全量导入模式暂不可用" msgstr "Full import mode is temporarily unavailable for security reasons" -#: bkuser/apis/web/data_source/views.py:238 +#: bkuser/apis/web/data_source/views.py:242 msgid "仅可更新实体类型数据源配置" msgstr "Only configurations of real type data sources can be updated" -#: bkuser/apis/web/data_source/views.py:288 +#: bkuser/apis/web/data_source/views.py:290 msgid "仅可重置实体类型数据源" msgstr "Only real type data sources can be reset" -#: bkuser/apis/web/data_source/views.py:469 +#: bkuser/apis/web/data_source/views.py:475 msgid "仅实体类型的本地数据源有提供导入模板" msgstr "Only local data sources of real type provide import templates" -#: bkuser/apis/web/data_source/views.py:491 +#: bkuser/apis/web/data_source/views.py:497 msgid "仅能导出实体类型的本地数据源数据" msgstr "Only data from local real type data sources can be exported" -#: bkuser/apis/web/data_source/views.py:516 +#: bkuser/apis/web/data_source/views.py:522 msgid "仅实体类型的本地数据源支持导入功能" msgstr "Only local real type data sources support import functions" -#: bkuser/apis/web/data_source/views.py:523 +#: bkuser/apis/web/data_source/views.py:529 msgid "文件格式异常" msgstr "File format exception" -#: bkuser/apis/web/data_source/views.py:573 +#: bkuser/apis/web/data_source/views.py:581 msgid "本地数据源不支持同步,请使用导入功能" msgstr "" "Local data source does not support synchronization, please use the import " "function" -#: bkuser/apis/web/data_source/views.py:576 +#: bkuser/apis/web/data_source/views.py:584 msgid "仅实体类型的数据源支持同步" msgstr "Only real type data sources support synchronization" -#: bkuser/apis/web/data_source/views.py:628 bkuser/common/error_codes.py:93 +#: bkuser/apis/web/data_source/views.py:638 bkuser/common/error_codes.py:93 msgid "数据源不存在" msgstr "Data source does not exist" -#: bkuser/apis/web/data_source/views.py:631 +#: bkuser/apis/web/data_source/views.py:641 msgid "仅实体类型的数据源有同步记录" msgstr "Only real type data sources have synchronization records" @@ -446,50 +446,50 @@ msgstr "" msgid "数据类型:{}" msgstr "Data type: {}" -#: bkuser/apis/web/organization/views/users.py:309 +#: bkuser/apis/web/organization/views/users.py:313 msgid "仅可创建属于当前租户的用户" msgstr "Only users belonging to the current tenant can be created" -#: bkuser/apis/web/organization/views/users.py:469 +#: bkuser/apis/web/organization/views/users.py:473 msgid "仅本地实名数据源支持更新用户信息" msgstr "Only local real-name data sources support updating user information" -#: bkuser/apis/web/organization/views/users.py:472 +#: bkuser/apis/web/organization/views/users.py:476 msgid "仅可更新非协同产生的租户用户" msgstr "Only tenant users not created through collaboration can be updated" -#: bkuser/apis/web/organization/views/users.py:489 +#: bkuser/apis/web/organization/views/users.py:493 msgid "当前用户不允许更新用户名" msgstr "The current user is not allowed to update the username" -#: bkuser/apis/web/organization/views/users.py:531 +#: bkuser/apis/web/organization/views/users.py:611 msgid "仅本地实名数据源支持删除用户" msgstr "Only local real-name data sources support deleting users" -#: bkuser/apis/web/organization/views/users.py:534 +#: bkuser/apis/web/organization/views/users.py:614 msgid "仅可删除非协同产生的租户用户" msgstr "Only tenant users not created through collaboration can be deleted" -#: bkuser/apis/web/organization/views/users.py:611 +#: bkuser/apis/web/organization/views/users.py:766 msgid "该租户用户没有可用的密码规则" msgstr "There are no available password rules for this tenant user" -#: bkuser/apis/web/organization/views/users.py:648 +#: bkuser/apis/web/organization/views/users.py:803 #: bkuser/apis/web/personal_center/views.py:517 msgid "仅可以重置 已经启用密码功能 的 本地数据源 的用户密码" msgstr "" "Password can only be reset for users of local data sources with the password " "function enabled" -#: bkuser/apis/web/organization/views/users.py:776 +#: bkuser/apis/web/organization/views/users.py:931 msgid "指定的租户部门不存在" msgstr "The specified tenant department does not exist" -#: bkuser/apis/web/organization/views/users.py:1104 +#: bkuser/apis/web/organization/views/users.py:1422 msgid "当前数据源未启用密码功能" msgstr "Current data source has not enabled password functionality" -#: bkuser/apis/web/password/constants.py:24 +#: bkuser/apis/web/password/constants.py:24 bkuser/apps/audit/constants.py:28 msgid "租户用户" msgstr "Tenant user" @@ -788,145 +788,184 @@ msgstr "Data Source" msgid "认证源" msgstr "IDP" -#: bkuser/apps/audit/constants.py:27 bkuser/apps/idp/constants.py:38 -#: bkuser/apps/sync/constants.py:79 bkuser/apps/sync/constants.py:89 -msgid "用户" -msgstr "User" +#: bkuser/apps/audit/constants.py:27 +msgid "数据源用户" +msgstr "Data Source User" -#: bkuser/apps/audit/constants.py:28 bkuser/apps/idp/constants.py:39 +#: bkuser/apps/audit/constants.py:29 bkuser/apps/idp/constants.py:39 #: bkuser/apps/sync/constants.py:80 bkuser/apps/sync/constants.py:90 msgid "部门" msgstr "Department" -#: bkuser/apps/audit/constants.py:29 bkuser/apps/idp/constants.py:41 +#: bkuser/apps/audit/constants.py:30 bkuser/apps/idp/constants.py:41 msgid "租户" msgstr "Tenant" -#: bkuser/apps/audit/constants.py:30 +#: bkuser/apps/audit/constants.py:31 msgid "虚拟用户" msgstr "Virtual User" -#: bkuser/apps/audit/constants.py:37 +#: bkuser/apps/audit/constants.py:38 msgid "创建数据源" msgstr "Create data source" -#: bkuser/apps/audit/constants.py:38 +#: bkuser/apps/audit/constants.py:39 msgid "修改数据源" msgstr "Modify data source" -#: bkuser/apps/audit/constants.py:39 -msgid "删除数据源" -msgstr "Delete data source" - #: bkuser/apps/audit/constants.py:40 +msgid "重置数据源" +msgstr "Reset data source" + +#: bkuser/apps/audit/constants.py:41 msgid "同步数据源" msgstr "Sync data source" -#: bkuser/apps/audit/constants.py:42 +#: bkuser/apps/audit/constants.py:43 msgid "创建认证源" msgstr "Create idp" -#: bkuser/apps/audit/constants.py:43 +#: bkuser/apps/audit/constants.py:44 msgid "修改认证源" msgstr "Modify idp" -#: bkuser/apps/audit/constants.py:44 +#: bkuser/apps/audit/constants.py:45 msgid "修改认证源状态" msgstr "Modify status of idp" #: bkuser/apps/audit/constants.py:46 -msgid "创建用户" -msgstr "Create user" - -#: bkuser/apps/audit/constants.py:47 -msgid "修改用户信息" -msgstr "Modify user" +msgid "重置认证源" +msgstr "Reset idp" #: bkuser/apps/audit/constants.py:48 -msgid "删除用户" -msgstr "Delete user" +msgid "创建数据源用户" +msgstr "Create data source user" #: bkuser/apps/audit/constants.py:49 -msgid "修改用户所属部门" -msgstr "Modify the user's department" +msgid "创建租户用户" +msgstr "Create tenant user" #: bkuser/apps/audit/constants.py:50 +msgid "创建用户-上级关系" +msgstr "Create user-leader relations" + +#: bkuser/apps/audit/constants.py:51 +msgid "创建用户-部门关系" +msgstr "Create user-department relations" + +#: bkuser/apps/audit/constants.py:52 +msgid "创建协同租户用户" +msgstr "Create collaboration tenant user" + +#: bkuser/apps/audit/constants.py:54 +msgid "修改数据源用户" +msgstr "Modify data source user" + +#: bkuser/apps/audit/constants.py:55 +msgid "修改租户用户" +msgstr "Modify tenant user" + +#: bkuser/apps/audit/constants.py:56 +msgid "修改用户-上级关系" +msgstr "Modify user-leader relations" + +#: bkuser/apps/audit/constants.py:57 +msgid "修改用户-部门关系" +msgstr "Modify user-department relations" + +#: bkuser/apps/audit/constants.py:59 +msgid "删除数据源用户" +msgstr "Delete data source user" + +#: bkuser/apps/audit/constants.py:60 +msgid "删除租户用户" +msgstr "Delete tenant user" + +#: bkuser/apps/audit/constants.py:61 +msgid "删除用户-上级关系" +msgstr "Delete user-leader relations" + +#: bkuser/apps/audit/constants.py:62 +msgid "删除用户-部门关系" +msgstr "Delete user-department relations" + +#: bkuser/apps/audit/constants.py:63 +msgid "删除协同租户用户" +msgstr "Delete collaboration tenant user" + +#: bkuser/apps/audit/constants.py:65 msgid "修改用户状态" msgstr "Modify the user's status" -#: bkuser/apps/audit/constants.py:51 +#: bkuser/apps/audit/constants.py:66 msgid "修改用户账号过期时间" msgstr "Modify the expiration time of the user account" -#: bkuser/apps/audit/constants.py:52 -msgid "修改用户上级" -msgstr "Modify the user's leader" - -#: bkuser/apps/audit/constants.py:53 +#: bkuser/apps/audit/constants.py:67 msgid "重置用户密码" msgstr "Reset user's password" -#: bkuser/apps/audit/constants.py:54 +#: bkuser/apps/audit/constants.py:68 msgid "修改用户邮箱" msgstr "Modify the user's email" -#: bkuser/apps/audit/constants.py:55 +#: bkuser/apps/audit/constants.py:69 msgid "修改用户电话号码" msgstr "Modify the user's phone number" -#: bkuser/apps/audit/constants.py:57 +#: bkuser/apps/audit/constants.py:71 msgid "创建部门" msgstr "Create department" -#: bkuser/apps/audit/constants.py:58 +#: bkuser/apps/audit/constants.py:72 msgid "修改部门名称" msgstr "Modify the name of department" -#: bkuser/apps/audit/constants.py:59 +#: bkuser/apps/audit/constants.py:73 msgid "删除部门" msgstr "Delete department" -#: bkuser/apps/audit/constants.py:60 +#: bkuser/apps/audit/constants.py:74 msgid "修改上级部门" msgstr "Modify the superior department" -#: bkuser/apps/audit/constants.py:62 +#: bkuser/apps/audit/constants.py:76 msgid "创建租户" msgstr "Create tenant" -#: bkuser/apps/audit/constants.py:63 +#: bkuser/apps/audit/constants.py:77 msgid "修改租户信息" msgstr "Modify tenant" -#: bkuser/apps/audit/constants.py:64 +#: bkuser/apps/audit/constants.py:78 msgid "删除租户" msgstr "Delete tenant" -#: bkuser/apps/audit/constants.py:65 +#: bkuser/apps/audit/constants.py:79 msgid "修改租户状态" msgstr "Modify the status of tenant" -#: bkuser/apps/audit/constants.py:66 -msgid "创建实名管理员" +#: bkuser/apps/audit/constants.py:80 +msgid "创建租户实名管理员" msgstr "Create real manager of tenant" -#: bkuser/apps/audit/constants.py:67 -msgid "删除实名管理员" +#: bkuser/apps/audit/constants.py:81 +msgid "删除租户实名管理员" msgstr "Delete real manager of tenant" -#: bkuser/apps/audit/constants.py:69 +#: bkuser/apps/audit/constants.py:83 msgid "修改租户账户有效期配置" msgstr "Modify the validity period config of tenant account" -#: bkuser/apps/audit/constants.py:72 +#: bkuser/apps/audit/constants.py:86 msgid "创建虚拟用户" msgstr "Create virtual user" -#: bkuser/apps/audit/constants.py:73 +#: bkuser/apps/audit/constants.py:87 msgid "修改虚拟用户信息" msgstr "Modify virtual user" -#: bkuser/apps/audit/constants.py:74 +#: bkuser/apps/audit/constants.py:88 msgid "删除虚拟用户" msgstr "Delete virtual user" @@ -967,6 +1006,11 @@ msgstr "Enabled" msgid "未启用" msgstr "Disabled" +#: bkuser/apps/idp/constants.py:38 bkuser/apps/sync/constants.py:79 +#: bkuser/apps/sync/constants.py:89 +msgid "用户" +msgstr "User" + #: bkuser/apps/idp/constants.py:42 msgid "任意" msgstr "Any" diff --git a/src/bk-user/locale/zh_cn/LC_MESSAGES/django.po b/src/bk-user/locale/zh_cn/LC_MESSAGES/django.po index cbf75cf1a..ad0b6c987 100644 --- a/src/bk-user/locale/zh_cn/LC_MESSAGES/django.po +++ b/src/bk-user/locale/zh_cn/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-30 16:12+0800\n" +"POT-Creation-Date: 2024-11-11 17:37+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -162,43 +162,43 @@ msgstr "待导入文件大小不得超过 {} M" msgid "出于安全考虑,全量导入模式暂不可用" msgstr "出于安全考虑,全量导入模式暂不可用" -#: bkuser/apis/web/data_source/views.py:238 +#: bkuser/apis/web/data_source/views.py:242 msgid "仅可更新实体类型数据源配置" msgstr "仅可更新实体类型数据源配置" -#: bkuser/apis/web/data_source/views.py:288 +#: bkuser/apis/web/data_source/views.py:290 msgid "仅可重置实体类型数据源" msgstr "仅可重置实体类型数据源" -#: bkuser/apis/web/data_source/views.py:469 +#: bkuser/apis/web/data_source/views.py:475 msgid "仅实体类型的本地数据源有提供导入模板" msgstr "仅实体类型的本地数据源有提供导入模板" -#: bkuser/apis/web/data_source/views.py:491 +#: bkuser/apis/web/data_source/views.py:497 msgid "仅能导出实体类型的本地数据源数据" msgstr "仅能导出实体类型的本地数据源数据" -#: bkuser/apis/web/data_source/views.py:516 +#: bkuser/apis/web/data_source/views.py:522 msgid "仅实体类型的本地数据源支持导入功能" msgstr "仅实体类型的本地数据源支持导入功能" -#: bkuser/apis/web/data_source/views.py:523 +#: bkuser/apis/web/data_source/views.py:529 msgid "文件格式异常" msgstr "文件格式异常" -#: bkuser/apis/web/data_source/views.py:573 +#: bkuser/apis/web/data_source/views.py:581 msgid "本地数据源不支持同步,请使用导入功能" msgstr "本地数据源不支持同步,请使用导入功能" -#: bkuser/apis/web/data_source/views.py:576 +#: bkuser/apis/web/data_source/views.py:584 msgid "仅实体类型的数据源支持同步" msgstr "仅实体类型的数据源支持同步" -#: bkuser/apis/web/data_source/views.py:628 bkuser/common/error_codes.py:93 +#: bkuser/apis/web/data_source/views.py:638 bkuser/common/error_codes.py:93 msgid "数据源不存在" msgstr "数据源不存在" -#: bkuser/apis/web/data_source/views.py:631 +#: bkuser/apis/web/data_source/views.py:641 msgid "仅实体类型的数据源有同步记录" msgstr "仅实体类型的数据源有同步记录" @@ -415,48 +415,48 @@ msgstr "多选枚举,多个值以 / 分隔,可选值:{}" msgid "数据类型:{}" msgstr "数据类型:{}" -#: bkuser/apis/web/organization/views/users.py:309 +#: bkuser/apis/web/organization/views/users.py:313 msgid "仅可创建属于当前租户的用户" msgstr "仅可创建属于当前租户的用户" -#: bkuser/apis/web/organization/views/users.py:469 +#: bkuser/apis/web/organization/views/users.py:473 msgid "仅本地实名数据源支持更新用户信息" msgstr "仅本地实名数据源支持更新用户信息" -#: bkuser/apis/web/organization/views/users.py:472 +#: bkuser/apis/web/organization/views/users.py:476 msgid "仅可更新非协同产生的租户用户" msgstr "仅可更新非协同产生的租户用户" -#: bkuser/apis/web/organization/views/users.py:489 +#: bkuser/apis/web/organization/views/users.py:493 msgid "当前用户不允许更新用户名" msgstr "当前用户不允许更新用户名" -#: bkuser/apis/web/organization/views/users.py:531 +#: bkuser/apis/web/organization/views/users.py:611 msgid "仅本地实名数据源支持删除用户" msgstr "仅本地实名数据源支持删除用户" -#: bkuser/apis/web/organization/views/users.py:534 +#: bkuser/apis/web/organization/views/users.py:614 msgid "仅可删除非协同产生的租户用户" msgstr "仅可删除非协同产生的租户用户" -#: bkuser/apis/web/organization/views/users.py:611 +#: bkuser/apis/web/organization/views/users.py:766 msgid "该租户用户没有可用的密码规则" msgstr "该租户用户没有可用的密码规则" -#: bkuser/apis/web/organization/views/users.py:648 +#: bkuser/apis/web/organization/views/users.py:803 #: bkuser/apis/web/personal_center/views.py:517 msgid "仅可以重置 已经启用密码功能 的 本地数据源 的用户密码" msgstr "仅可以重置 已经启用密码功能 的 本地数据源 的用户密码" -#: bkuser/apis/web/organization/views/users.py:776 +#: bkuser/apis/web/organization/views/users.py:931 msgid "指定的租户部门不存在" msgstr "指定的租户部门不存在" -#: bkuser/apis/web/organization/views/users.py:1104 +#: bkuser/apis/web/organization/views/users.py:1422 msgid "当前数据源未启用密码功能" msgstr "当前数据源未启用密码功能" -#: bkuser/apis/web/password/constants.py:24 +#: bkuser/apis/web/password/constants.py:24 bkuser/apps/audit/constants.py:28 msgid "租户用户" msgstr "租户用户" @@ -730,145 +730,184 @@ msgstr "数据源" msgid "认证源" msgstr "认证源" -#: bkuser/apps/audit/constants.py:27 bkuser/apps/idp/constants.py:38 -#: bkuser/apps/sync/constants.py:79 bkuser/apps/sync/constants.py:89 -msgid "用户" -msgstr "用户" +#: bkuser/apps/audit/constants.py:27 +msgid "数据源用户" +msgstr "数据源用户" -#: bkuser/apps/audit/constants.py:28 bkuser/apps/idp/constants.py:39 +#: bkuser/apps/audit/constants.py:29 bkuser/apps/idp/constants.py:39 #: bkuser/apps/sync/constants.py:80 bkuser/apps/sync/constants.py:90 msgid "部门" msgstr "部门" -#: bkuser/apps/audit/constants.py:29 bkuser/apps/idp/constants.py:41 +#: bkuser/apps/audit/constants.py:30 bkuser/apps/idp/constants.py:41 msgid "租户" msgstr "租户" -#: bkuser/apps/audit/constants.py:30 +#: bkuser/apps/audit/constants.py:31 msgid "虚拟用户" msgstr "虚拟用户" -#: bkuser/apps/audit/constants.py:37 +#: bkuser/apps/audit/constants.py:38 msgid "创建数据源" msgstr "创建数据源" -#: bkuser/apps/audit/constants.py:38 +#: bkuser/apps/audit/constants.py:39 msgid "修改数据源" msgstr "修改数据源" -#: bkuser/apps/audit/constants.py:39 -msgid "删除数据源" -msgstr "删除数据源" - #: bkuser/apps/audit/constants.py:40 +msgid "重置数据源" +msgstr "重置数据源" + +#: bkuser/apps/audit/constants.py:41 msgid "同步数据源" msgstr "同步数据源" -#: bkuser/apps/audit/constants.py:42 +#: bkuser/apps/audit/constants.py:43 msgid "创建认证源" msgstr "创建认证源" -#: bkuser/apps/audit/constants.py:43 +#: bkuser/apps/audit/constants.py:44 msgid "修改认证源" msgstr "修改认证源" -#: bkuser/apps/audit/constants.py:44 +#: bkuser/apps/audit/constants.py:45 msgid "修改认证源状态" msgstr "修改认证源状态" #: bkuser/apps/audit/constants.py:46 -msgid "创建用户" -msgstr "创建用户" - -#: bkuser/apps/audit/constants.py:47 -msgid "修改用户信息" -msgstr "修改用户信息" +msgid "重置认证源" +msgstr "重置认证源" #: bkuser/apps/audit/constants.py:48 -msgid "删除用户" -msgstr "删除用户" +msgid "创建数据源用户" +msgstr "创建数据源用户" #: bkuser/apps/audit/constants.py:49 -msgid "修改用户所属部门" -msgstr "修改用户所属部门" +msgid "创建租户用户" +msgstr "创建租户用户" #: bkuser/apps/audit/constants.py:50 +msgid "创建用户-上级关系" +msgstr "创建用户-上级关系" + +#: bkuser/apps/audit/constants.py:51 +msgid "创建用户-部门关系" +msgstr "创建用户-部门关系" + +#: bkuser/apps/audit/constants.py:52 +msgid "创建协同租户用户" +msgstr "创建协同租户用户" + +#: bkuser/apps/audit/constants.py:54 +msgid "修改数据源用户" +msgstr "修改数据源用户" + +#: bkuser/apps/audit/constants.py:55 +msgid "修改租户用户" +msgstr "修改租户用户" + +#: bkuser/apps/audit/constants.py:56 +msgid "修改用户-上级关系" +msgstr "修改用户-上级关系" + +#: bkuser/apps/audit/constants.py:57 +msgid "修改用户-部门关系" +msgstr "修改用户-部门关系" + +#: bkuser/apps/audit/constants.py:59 +msgid "删除数据源用户" +msgstr "删除数据源用户" + +#: bkuser/apps/audit/constants.py:60 +msgid "删除租户用户" +msgstr "删除租户用户" + +#: bkuser/apps/audit/constants.py:61 +msgid "删除用户-上级关系" +msgstr "删除用户-上级关系" + +#: bkuser/apps/audit/constants.py:62 +msgid "删除用户-部门关系" +msgstr "删除用户-部门关系" + +#: bkuser/apps/audit/constants.py:63 +msgid "删除协同租户用户" +msgstr "删除协同租户用户" + +#: bkuser/apps/audit/constants.py:65 msgid "修改用户状态" msgstr "修改用户状态" -#: bkuser/apps/audit/constants.py:51 +#: bkuser/apps/audit/constants.py:66 msgid "修改用户账号过期时间" msgstr "修改用户账号过期时间" -#: bkuser/apps/audit/constants.py:52 -msgid "修改用户上级" -msgstr "修改用户上级" - -#: bkuser/apps/audit/constants.py:53 +#: bkuser/apps/audit/constants.py:67 msgid "重置用户密码" msgstr "重置用户密码" -#: bkuser/apps/audit/constants.py:54 +#: bkuser/apps/audit/constants.py:68 msgid "修改用户邮箱" msgstr "修改用户邮箱" -#: bkuser/apps/audit/constants.py:55 +#: bkuser/apps/audit/constants.py:69 msgid "修改用户电话号码" msgstr "修改用户电话号码" -#: bkuser/apps/audit/constants.py:57 +#: bkuser/apps/audit/constants.py:71 msgid "创建部门" msgstr "创建部门" -#: bkuser/apps/audit/constants.py:58 +#: bkuser/apps/audit/constants.py:72 msgid "修改部门名称" msgstr "修改部门名称" -#: bkuser/apps/audit/constants.py:59 +#: bkuser/apps/audit/constants.py:73 msgid "删除部门" msgstr "删除部门" -#: bkuser/apps/audit/constants.py:60 +#: bkuser/apps/audit/constants.py:74 msgid "修改上级部门" msgstr "修改上级部门" -#: bkuser/apps/audit/constants.py:62 +#: bkuser/apps/audit/constants.py:76 msgid "创建租户" msgstr "创建租户" -#: bkuser/apps/audit/constants.py:63 +#: bkuser/apps/audit/constants.py:77 msgid "修改租户信息" msgstr "修改租户信息" -#: bkuser/apps/audit/constants.py:64 +#: bkuser/apps/audit/constants.py:78 msgid "删除租户" msgstr "删除租户" -#: bkuser/apps/audit/constants.py:65 +#: bkuser/apps/audit/constants.py:79 msgid "修改租户状态" msgstr "修改租户状态" -#: bkuser/apps/audit/constants.py:66 -msgid "创建实名管理员" -msgstr "创建实名管理员" +#: bkuser/apps/audit/constants.py:80 +msgid "创建租户实名管理员" +msgstr "创建租户实名管理员" -#: bkuser/apps/audit/constants.py:67 -msgid "删除实名管理员" -msgstr "删除实名管理员" +#: bkuser/apps/audit/constants.py:81 +msgid "删除租户实名管理员" +msgstr "删除租户实名管理员" -#: bkuser/apps/audit/constants.py:69 +#: bkuser/apps/audit/constants.py:83 msgid "修改租户账户有效期配置" msgstr "修改租户账户有效期配置" -#: bkuser/apps/audit/constants.py:72 +#: bkuser/apps/audit/constants.py:86 msgid "创建虚拟用户" msgstr "创建虚拟用户" -#: bkuser/apps/audit/constants.py:73 +#: bkuser/apps/audit/constants.py:87 msgid "修改虚拟用户信息" msgstr "修改虚拟用户信息" -#: bkuser/apps/audit/constants.py:74 +#: bkuser/apps/audit/constants.py:88 msgid "删除虚拟用户" msgstr "删除虚拟用户" @@ -909,6 +948,11 @@ msgstr "启用" msgid "未启用" msgstr "未启用" +#: bkuser/apps/idp/constants.py:38 bkuser/apps/sync/constants.py:79 +#: bkuser/apps/sync/constants.py:89 +msgid "用户" +msgstr "用户" + #: bkuser/apps/idp/constants.py:42 msgid "任意" msgstr "任意"