From db6459d3dbe8c7c88510fb6c983a4ed6f5853dfb Mon Sep 17 00:00:00 2001 From: rolinchen Date: Mon, 25 Nov 2024 15:40:44 +0800 Subject: [PATCH 1/6] feat: added operation audit for data source and users --- .../bkuser/apis/web/data_source/views.py | 56 ++-- .../apis/web/organization/views/relations.py | 37 +++ .../apis/web/organization/views/users.py | 209 ++++++++++++ src/bk-user/bkuser/apps/audit/data_models.py | 10 + src/bk-user/bkuser/apps/audit/recorder.py | 53 +-- src/bk-user/bkuser/biz/auditor.py | 305 ++++++++++++++++++ src/bk-user/bkuser/utils/django.py | 31 ++ 7 files changed, 653 insertions(+), 48 deletions(-) create mode 100644 src/bk-user/bkuser/biz/auditor.py create mode 100644 src/bk-user/bkuser/utils/django.py 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 0a4401089..208ebcbc1 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_models 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,7 @@ 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_after=get_model_dict(ds), ) return Response( @@ -253,11 +251,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 +268,8 @@ 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), ) return Response(status=status.HTTP_204_NO_CONTENT) @@ -308,14 +303,23 @@ 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.DELETE_DATA_SOURCE, + data_before=get_model_dict(data_source), ) - 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.DELETE_IDP, + data_before=get_model_dict(data_before_idp), + ) + for data_before_idp in list(waiting_delete_idps) + ] with transaction.atomic(): # 删除认证源敏感信息 @@ -334,20 +338,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) 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..2ebdbe6cc 100644 --- a/src/bk-user/bkuser/apis/web/organization/views/relations.py +++ b/src/bk-user/bkuser/apis/web/organization/views/relations.py @@ -34,6 +34,7 @@ from bkuser.apps.permission.constants import PermAction from bkuser.apps.permission.permissions import perm_class from bkuser.apps.tenant.models import TenantDepartment, TenantUser +from bkuser.biz.auditor import TenantUserDepartmentRelationsAuditor class TenantDeptUserRelationBatchCreateApi(CurrentUserTenantDataSourceMixin, generics.CreateAPIView): @@ -66,6 +67,12 @@ def post(self, request, *args, **kwargs): id__in=data["user_ids"], ).values_list("data_source_user_id", flat=True) + # 【审计】创建审计对象并记录变更前的数据 + auditor = TenantUserDepartmentRelationsAuditor( + operator=request.user.username, tenant_id=cur_tenant_id, data_source_user_ids=data_source_user_ids + ) + auditor.pre_record_data_before() + # 复制操作:为数据源部门 & 用户添加关联边,但是不会影响存量的关联边 relations = [ DataSourceDepartmentUserRelation(user_id=user_id, department_id=dept_id, data_source=data_source) @@ -74,6 +81,9 @@ def post(self, request, *args, **kwargs): # 由于复制操作不会影响存量的关联边,所以需要忽略冲突,避免出现用户复选的情况 DataSourceDepartmentUserRelation.objects.bulk_create(relations, ignore_conflicts=True) + # 【审计】将审计记录保存至数据库 + auditor.batch_record(extras={"department_ids": list(data_source_dept_ids)}) + return Response(status=status.HTTP_204_NO_CONTENT) @@ -107,6 +117,12 @@ def put(self, request, *args, **kwargs): id__in=data["user_ids"], ).values_list("data_source_user_id", flat=True) + # 【审计】创建审计对象并记录变更前的数据 + auditor = TenantUserDepartmentRelationsAuditor( + operator=request.user.username, tenant_id=cur_tenant_id, data_source_user_ids=data_source_user_ids + ) + auditor.pre_record_data_before() + # 移动操作:为数据源部门 & 用户添加关联边,但是会删除这批用户所有的存量关联边 with transaction.atomic(): # 先删除 @@ -118,6 +134,9 @@ def put(self, request, *args, **kwargs): ] DataSourceDepartmentUserRelation.objects.bulk_create(relations) + # 【审计】将审计记录保存至数据库 + auditor.batch_record(extras={"department_ids": list(data_source_dept_ids)}) + return Response(status=status.HTTP_204_NO_CONTENT) @swagger_auto_schema( @@ -147,6 +166,12 @@ def patch(self, request, *args, **kwargs): id__in=data["user_ids"], ).values_list("data_source_user_id", flat=True) + # 【审计】创建审计对象 + auditor = TenantUserDepartmentRelationsAuditor( + operator=request.user.username, tenant_id=cur_tenant_id, data_source_user_ids=data_source_user_ids + ) + auditor.pre_record_data_before() + # 移动操作:为数据源部门 & 用户添加关联边,但是会删除这批用户在当前部门的存量关联边 with transaction.atomic(): # 先删除(仅限于指定部门) @@ -160,6 +185,9 @@ def patch(self, request, *args, **kwargs): ] DataSourceDepartmentUserRelation.objects.bulk_create(relations, ignore_conflicts=True) + # 【审计】将审计记录保存至数据库 + auditor.batch_record(extras={"department_id": source_data_source_dept.id}) + return Response(status=status.HTTP_204_NO_CONTENT) @@ -191,8 +219,17 @@ def delete(self, request, *args, **kwargs): id__in=data["user_ids"], ).values_list("data_source_user_id", flat=True) + # 【审计】创建审计对象 + auditor = TenantUserDepartmentRelationsAuditor( + operator=request.user.username, tenant_id=cur_tenant_id, data_source_user_ids=data_source_user_ids + ) + auditor.pre_record_data_before() + DataSourceDepartmentUserRelation.objects.filter( user_id__in=data_source_user_ids, department=source_data_source_dept ).delete() + # 【审计】将审计记录保存至数据库 + auditor.batch_record(extras={"department_id": source_data_source_dept.id}) + 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..84c356622 100644 --- a/src/bk-user/bkuser/apis/web/organization/views/users.py +++ b/src/bk-user/bkuser/apis/web/organization/views/users.py @@ -58,6 +58,9 @@ TenantUserUpdateInputSLZ, ) from bkuser.apis.web.organization.views.mixins import CurrentUserTenantDataSourceMixin +from bkuser.apps.audit.constants import ObjectTypeEnum, OperationEnum +from bkuser.apps.audit.data_models 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, @@ -79,6 +82,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 +500,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 +529,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 +547,12 @@ 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) + auditor.record(tenant_user, data_source_user) + with transaction.atomic(): # 删除用户意味着租户用户 & 数据源用户都删除,前面检查过权限, # 因此这里所有协同产生的租户用户也需要删除(不等同步,立即生效) @@ -543,6 +562,9 @@ def delete(self, request, *args, **kwargs): DataSourceUserLeaderRelation.objects.filter(leader=data_source_user).delete() data_source_user.delete() + # 【审计】保存记录至数据库 + auditor.save_audit_records() + return Response(status=status.HTTP_204_NO_CONTENT) @@ -572,6 +594,12 @@ def put(self, request, *args, **kwargs): slz.is_valid(raise_exception=True) data = slz.validated_data + # 【审计】记录变更前的租户用户账号有效期与状态 + data_before = { + "account_expired_at": tenant_user.account_expired_at.strftime("%Y-%m-%d %H:%M:%S"), + "status": tenant_user.status, + } + tenant_user.account_expired_at = data["account_expired_at"] tenant_user.updater = request.user.username @@ -581,6 +609,20 @@ def put(self, request, *args, **kwargs): tenant_user.save(update_fields=["account_expired_at", "status", "updater", "updated_at"]) + # 记录审计 + add_audit_record( + operator=request.user.username, + tenant_id=self.get_current_tenant_id(), + operation=OperationEnum.MODIFY_USER_ACCOUNT_EXPIRED_AT, + object_type=ObjectTypeEnum.TENANT_USER, + object_id=tenant_user.id, + data_before=data_before, + data_after={ + "account_expired_at": tenant_user.account_expired_at.strftime("%Y-%m-%d %H:%M:%S"), + "status": tenant_user.status, + }, + ) + return Response(status=status.HTTP_204_NO_CONTENT) @@ -666,6 +708,17 @@ def put(self, request, *args, **kwargs): operator=request.user.username, ) + # 记录审计 + add_audit_record( + operator=request.user.username, + tenant_id=self.get_current_tenant_id(), + operation=OperationEnum.MODIFY_USER_PASSWORD, + object_type=ObjectTypeEnum.DATA_SOURCE_USER, + object_id=data_source_user.id, + object_name=data_source_user.username, + extras={"valid_days": plugin_config.password_expire.valid_time}, + ) + # 发送新密码通知到用户 send_reset_password_to_user.delay(data_source_user.id, raw_password) return Response(status=status.HTTP_204_NO_CONTENT) @@ -731,6 +784,10 @@ def get_queryset(self) -> QuerySet[TenantUser]: ) def put(self, request, *args, **kwargs): tenant_user = self.get_object() + + # 【审计】记录变更前的租户用户状态 + data_before_status = {"status": tenant_user.status} + # 正常 / 过期的租户用户都可以停用 if tenant_user.status in [TenantUserStatus.ENABLED, TenantUserStatus.EXPIRED]: tenant_user.status = TenantUserStatus.DISABLED @@ -744,6 +801,18 @@ def put(self, request, *args, **kwargs): tenant_user.updater = request.user.username tenant_user.save(update_fields=["status", "updater", "updated_at"]) + + # 记录审计 + add_audit_record( + operator=request.user.username, + tenant_id=self.get_current_tenant_id(), + operation=OperationEnum.MODIFY_USER_STATUS, + object_type=ObjectTypeEnum.TENANT_USER, + object_id=tenant_user.id, + data_before=data_before_status, + data_after={"status": tenant_user.status}, + ) + return Response(TenantUserStatusUpdateOutputSLZ(tenant_user).data, status=status.HTTP_200_OK) @@ -807,6 +876,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) @@ -922,6 +1000,14 @@ 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) + auditor.batch_record(data_before_tenant_users) + with transaction.atomic(): # 删除用户意味着租户用户 & 数据源用户都删除,前面检查过权限, # 因此这里所有协同产生的租户用户也需要删除(不等同步,立即生效) @@ -935,6 +1021,9 @@ def delete(self, request, *args, **kwargs): # 最后才是批量回收数据源用户 DataSourceUser.objects.filter(id__in=data_source_user_ids).delete() + # 【审计】保存记录至数据库 + auditor.save_audit_records() + return Response(status=status.HTTP_204_NO_CONTENT) @@ -961,6 +1050,17 @@ def put(self, request, *args, **kwargs): slz.is_valid(raise_exception=True) data = slz.validated_data + # 【审计】记录变更前的租户用户数据 + data_before_tenant_users = list(TenantUser.objects.filter(id__in=data["user_ids"], tenant_id=cur_tenant_id)) + + data_before_dict = { + user.id: { + "account_expired_at": user.account_expired_at.strftime("%Y-%m-%d %H:%M:%S"), + "status": user.status, + } + for user in data_before_tenant_users + } + with transaction.atomic(): # 根据租户用户当前状态判断,如果是过期状态则转为正常 TenantUser.objects.filter( @@ -973,6 +1073,31 @@ def put(self, request, *args, **kwargs): updated_at=timezone.now(), ) + # 【审计】记录变更后的租户用户数据 + data_after_tenant_users = list(TenantUser.objects.filter(id__in=data["user_ids"], tenant_id=cur_tenant_id)) + + # 【审计】批量创建 AuditObject 对象 + audit_objects = [ + AuditObject( + id=user.id, + type=ObjectTypeEnum.TENANT_USER, + operation=OperationEnum.MODIFY_USER_ACCOUNT_EXPIRED_AT, + data_before=data_before_dict[user.id], + data_after={ + "account_expired_at": user.account_expired_at.strftime("%Y-%m-%d %H:%M:%S"), + "status": user.status, + }, + ) + for user in data_after_tenant_users + ] + + # 记录审计 + batch_add_audit_records( + operator=request.user.username, + tenant_id=cur_tenant_id, + objects=audit_objects, + ) + return Response(status=status.HTTP_204_NO_CONTENT) @@ -1004,6 +1129,8 @@ def put(self, request, *args, **kwargs): now = timezone.now() updater = request.user.username + data_before_dict = {user.id: user.status for user in tenant_users} + # 停用的时候,正常 / 过期的租户用户都直接停用 if data["status"] == TenantUserStatus.DISABLED: tenant_users.update( @@ -1027,6 +1154,28 @@ def put(self, request, *args, **kwargs): updated_at=now, ) + # 【审计】记录变更后的租户用户数据 + data_after_tenant_users = TenantUser.objects.filter(id__in=data["user_ids"], tenant_id=cur_tenant_id) + + # 【审计】批量创建 AuditObject 对象 + audit_objects = [ + AuditObject( + id=user.id, + type=ObjectTypeEnum.TENANT_USER, + operation=OperationEnum.MODIFY_USER_STATUS, + data_before={"status": data_before_dict[user.id]}, + data_after={"status": user.status}, + ) + for user in data_after_tenant_users + ] + + # 记录审计 + batch_add_audit_records( + operator=request.user.username, + tenant_id=cur_tenant_id, + objects=audit_objects, + ) + return Response(status=status.HTTP_204_NO_CONTENT) @@ -1067,6 +1216,9 @@ def put(self, request, *args, **kwargs): for leader_id, user_id in itertools.product(leader_ids, data_source_user_ids) ] + # 【审计】记录变更前的用户 - 上级映射 + data_before_user_leader_map = self.get_user_leader_map(data_source_user_ids=data_source_user_ids) + with transaction.atomic(): # 先删除现有的用户 - 上级关系 DataSourceUserLeaderRelation.objects.filter(user_id__in=data_source_user_ids).delete() @@ -1074,8 +1226,46 @@ def put(self, request, *args, **kwargs): # 再添加新的用户 - 上级关系 DataSourceUserLeaderRelation.objects.bulk_create(relations) + # 【审计】批量创建 AuditObject 对象 + audit_objects = [ + AuditObject( + id=tenant_user.id, + type=ObjectTypeEnum.DATA_SOURCE_USER, + name=tenant_user.data_source_user.username, + operation=OperationEnum.MODIFY_USER_LEADER, + data_before={"leader_ids": data_before_user_leader_map[tenant_user.data_source_user_id]}, + data_after={"leader_ids": list(leader_ids)}, + extras={"leader_ids": list(leader_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) + @staticmethod + def get_user_leader_map(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 + class TenantUserPasswordBatchResetApi( CurrentUserTenantDataSourceMixin, ExcludePatchAPIViewMixin, generics.UpdateAPIView @@ -1133,6 +1323,25 @@ def put(self, request, *args, **kwargs): for data_source_user in data_source_users: send_reset_password_to_user.delay(data_source_user.id, raw_password) + # 【审计】批量创建 AuditObject 对象 + audit_objects = [ + AuditObject( + id=user.id, + type=ObjectTypeEnum.DATA_SOURCE_USER, + operation=OperationEnum.MODIFY_USER_PASSWORD, + name=user.username, + extras={"valid_days": plugin_config.password_expire.valid_time}, + ) + for user in data_source_users + ] + + # 记录审计 + 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/apps/audit/data_models.py b/src/bk-user/bkuser/apps/audit/data_models.py index bcdc4cf6c..3282b470e 100644 --- a/src/bk-user/bkuser/apps/audit/data_models.py +++ b/src/bk-user/bkuser/apps/audit/data_models.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/recorder.py b/src/bk-user/bkuser/apps/audit/recorder.py index 2aaa2cef2..f587f1bb0 100644 --- a/src/bk-user/bkuser/apps/audit/recorder.py +++ b/src/bk-user/bkuser/apps/audit/recorder.py @@ -15,7 +15,7 @@ # 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 typing import Any, Dict, List from bkuser.utils.uuid import generate_uuid @@ -30,7 +30,10 @@ def add_audit_record( operation: OperationEnum, object_type: ObjectTypeEnum, object_id: str | int, - extras: Dict | None = None, + object_name: str = "", + data_before: Dict[str, Any] | None = None, + data_after: Dict[str, Any] | None = None, + extras: Dict[str, Any] | None = None, ): """ 添加操作审计记录 @@ -40,23 +43,34 @@ 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 {}, - ) + + data_before = data_before or {} + data_after = data_after or {} + 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=data_before, + data_after=data_after, + extras=extras, + ) def batch_add_audit_records( operator: str, tenant_id: str, - operation: OperationEnum, - object_type: ObjectTypeEnum, objects: List[AuditObject], ): """ @@ -64,9 +78,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 +88,17 @@ 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=obj.data_before, + data_after=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) diff --git a/src/bk-user/bkuser/biz/auditor.py b/src/bk-user/bkuser/biz/auditor.py new file mode 100644 index 000000000..64c303b53 --- /dev/null +++ b/src/bk-user/bkuser/biz/auditor.py @@ -0,0 +1,305 @@ +# -*- 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 collections import defaultdict +from typing import Dict, List + +from bkuser.apps.audit.constants import ObjectTypeEnum, OperationEnum +from bkuser.apps.audit.data_models 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": list( + DataSourceDepartmentUserRelation.objects.filter( + user=data_source_user, + ).values_list("department_id", flat=True) + ), + # 记录修改前的用户上级 + "leader_ids": list( + DataSourceUserLeaderRelation.objects.filter(user=data_source_user).values_list("leader_id", flat=True) + ), + } + + # 记录修改前的协同租户用户 + 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 + } + + def batch_pre_record_data_before(self, tenant_users: List[TenantUser]): + """批量记录变更前的相关数据记录""" + + for tenant_user in tenant_users: + self.pre_record_data_before(tenant_user, 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: OperationEnum.DELETE_USER_DEPARTMENT, + OperationEnum.MODIFY_USER_LEADER: OperationEnum.DELETE_USER_LEADER, + 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 模块里的方法进行记录""" + tenant_user_id = tenant_user.id + + 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.extend( + [ + # 数据源用户本身信息 + 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), + 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), + 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, + 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) + + +class TenantUserDepartmentRelationsAuditor: + """用于记录用户-部门关系变更的审计""" + + def __init__(self, operator: str, tenant_id: str, data_source_user_ids: List[int]): + self.operator = operator + self.tenant_id = tenant_id + self.audit_objects: List[AuditObject] = [] + self.data_before: Dict[int, Dict] = {} + self.data_source_user_ids = data_source_user_ids + + def pre_record_data_before(self): + """记录变更前的相关数据记录""" + # 获取用户与部门之间的映射关系 + data_before_map = self.get_user_department_map(self.data_source_user_ids) + + # 初始化 data_before, 记录变更前用户与部门之间的映射关系 + for data_source_user_id in self.data_source_user_ids: + self.data_before[data_source_user_id] = {"department_ids": data_before_map.get(data_source_user_id, [])} + + def record(self, data_source_user: DataSourceUser, data_before: Dict, data_after: Dict, extras: Dict): + """调用 apps.audit 模块里的方法进行记录""" + self.audit_objects.append( + AuditObject( + id=data_source_user.id, + name=data_source_user.username, + type=ObjectTypeEnum.DATA_SOURCE_USER, + operation=OperationEnum.MODIFY_USER_DEPARTMENT, + data_before=data_before, + data_after=data_after, + extras=extras, + ) + ) + + def batch_record(self, extras: Dict[str, List]): + """批量记录""" + data_source_users = DataSourceUser.objects.filter( + id__in=self.data_source_user_ids, + ) + # 记录变更后的用户与部门之间的映射关系 + data_after_map = self.get_user_department_map(self.data_source_user_ids) + + for data_source_user in data_source_users: + data_before = self.data_before[data_source_user.id] + data_after = {"department_ids": data_after_map.get(data_source_user.id, [])} + self.record(data_source_user, data_before, data_after, extras) + batch_add_audit_records(self.operator, self.tenant_id, self.audit_objects) + + @staticmethod + def get_user_department_map(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 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)) From 64add039c6968b2cd8c659ee18bbb4051e3a18ba Mon Sep 17 00:00:00 2001 From: rolinchen Date: Mon, 25 Nov 2024 16:21:28 +0800 Subject: [PATCH 2/6] feat: added operation audit for data source and users --- .../apis/web/organization/views/relations.py | 2 +- .../bkuser/apis/web/organization/views/users.py | 14 ++++++-------- src/bk-user/bkuser/apps/audit/constants.py | 14 +++++++++----- src/bk-user/bkuser/biz/auditor.py | 4 ++-- 4 files changed, 18 insertions(+), 16 deletions(-) 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 2ebdbe6cc..054075e09 100644 --- a/src/bk-user/bkuser/apis/web/organization/views/relations.py +++ b/src/bk-user/bkuser/apis/web/organization/views/relations.py @@ -82,7 +82,7 @@ def post(self, request, *args, **kwargs): DataSourceDepartmentUserRelation.objects.bulk_create(relations, ignore_conflicts=True) # 【审计】将审计记录保存至数据库 - auditor.batch_record(extras={"department_ids": list(data_source_dept_ids)}) + auditor.batch_record() 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 84c356622..d67b84d0e 100644 --- a/src/bk-user/bkuser/apis/web/organization/views/users.py +++ b/src/bk-user/bkuser/apis/web/organization/views/users.py @@ -1229,18 +1229,16 @@ def put(self, request, *args, **kwargs): # 【审计】批量创建 AuditObject 对象 audit_objects = [ AuditObject( - id=tenant_user.id, + id=data_source_user.id, type=ObjectTypeEnum.DATA_SOURCE_USER, - name=tenant_user.data_source_user.username, + name=data_source_user.username, operation=OperationEnum.MODIFY_USER_LEADER, - data_before={"leader_ids": data_before_user_leader_map[tenant_user.data_source_user_id]}, + data_before={"leader_ids": data_before_user_leader_map[data_source_user.id]}, data_after={"leader_ids": list(leader_ids)}, - extras={"leader_ids": list(leader_ids)}, ) - for tenant_user in TenantUser.objects.filter( - tenant_id=cur_tenant_id, - id__in=data["user_ids"], - ).select_related("data_source_user") + for data_source_user in DataSourceUser.objects.filter( + id__in=data_source_user_ids, + ) ] # 记录审计 diff --git a/src/bk-user/bkuser/apps/audit/constants.py b/src/bk-user/bkuser/apps/audit/constants.py index 66a8432c9..b1c41e9ed 100644 --- a/src/bk-user/bkuser/apps/audit/constants.py +++ b/src/bk-user/bkuser/apps/audit/constants.py @@ -39,11 +39,13 @@ class OperationEnum(StrStructuredEnum): MODIFY_DATA_SOURCE = EnumField("modify_data_source", label=_("修改数据源")) DELETE_DATA_SOURCE = EnumField("delete_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=_("修改认证源状态")) DELETE_IDP = EnumField("delete_idp", label=_("删除认证源")) + # 用户 CREATE_DATA_SOURCE_USER = EnumField("create_data_source_user", label=_("创建数据源用户")) CREATE_TENANT_USER = EnumField("create_tenant_user", label=_("创建租户用户")) @@ -55,6 +57,11 @@ class OperationEnum(StrStructuredEnum): MODIFY_TENANT_USER = EnumField("modify_tenant_user", label=_("修改租户用户")) MODIFY_USER_LEADER = EnumField("modify_user_leader", label=_("修改用户-上级关系")) MODIFY_USER_DEPARTMENT = EnumField("modify_user_department", label=_("修改用户-部门关系")) + MODIFY_USER_STATUS = EnumField("modify_user_status", label=_("修改用户状态")) + MODIFY_USER_ACCOUNT_EXPIRED_AT = EnumField("modify_user_account_expired_at", 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=_("修改用户电话号码")) DELETE_DATA_SOURCE_USER = EnumField("delete_data_source_user", label=_("删除数据源用户")) DELETE_TENANT_USER = EnumField("delete_tenant_user", label=_("删除租户用户")) @@ -62,16 +69,12 @@ class OperationEnum(StrStructuredEnum): DELETE_USER_DEPARTMENT = EnumField("delete_user_department", 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_PASSWORD = EnumField("modify_user_password", label=_("重置用户密码")) - MODIFY_USER_EMAIL = EnumField("modify_user_email", label=_("修改用户邮箱")) - MODIFY_USER_PHONE = EnumField("modify_user_phone", label=_("修改用户电话号码")) # 部门 CREATE_DEPARTMENT = EnumField("create_department", label=_("创建部门")) MODIFY_DEPARTMENT = EnumField("modify_department", label=_("修改部门名称")) DELETE_DEPARTMENT = EnumField("delete_department", label=_("删除部门")) MODIFY_PARENT_DEPARTMENT = EnumField("modify_parent_department", label=_("修改上级部门")) + # 租户 CREATE_TENANT = EnumField("create_tenant", label=_("创建租户")) MODIFY_TENANT = EnumField("modify_tenant", label=_("修改租户信息")) @@ -82,6 +85,7 @@ class OperationEnum(StrStructuredEnum): MODIFY_TENANT_ACCOUNT_VALIDITY_PERIOD_CONFIG = EnumField( "modify_tenant_account_validity_period_config", label=_("修改租户账户有效期配置") ) + # 虚拟用户 CREATE_VIRTUAL_USER = EnumField("create_virtual_user", label=_("创建虚拟用户")) MODIFY_VIRTUAL_USER = EnumField("modify_virtual_user", label=_("修改虚拟用户信息")) diff --git a/src/bk-user/bkuser/biz/auditor.py b/src/bk-user/bkuser/biz/auditor.py index 64c303b53..8e274e917 100644 --- a/src/bk-user/bkuser/biz/auditor.py +++ b/src/bk-user/bkuser/biz/auditor.py @@ -276,7 +276,7 @@ def record(self, data_source_user: DataSourceUser, data_before: Dict, data_after ) ) - def batch_record(self, extras: Dict[str, List]): + def batch_record(self, extras: Dict[str, List] | None = None): """批量记录""" data_source_users = DataSourceUser.objects.filter( id__in=self.data_source_user_ids, @@ -287,7 +287,7 @@ def batch_record(self, extras: Dict[str, List]): for data_source_user in data_source_users: data_before = self.data_before[data_source_user.id] data_after = {"department_ids": data_after_map.get(data_source_user.id, [])} - self.record(data_source_user, data_before, data_after, extras) + self.record(data_source_user, data_before, data_after, extras or {}) batch_add_audit_records(self.operator, self.tenant_id, self.audit_objects) @staticmethod From cd0205951cc7de3d84b34a91433cae3dac92427f Mon Sep 17 00:00:00 2001 From: rolinchen Date: Mon, 25 Nov 2024 17:11:46 +0800 Subject: [PATCH 3/6] feat: added operation audit for data source and users --- src/bk-user/bkuser/biz/auditor.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/bk-user/bkuser/biz/auditor.py b/src/bk-user/bkuser/biz/auditor.py index 8e274e917..3aadbb66b 100644 --- a/src/bk-user/bkuser/biz/auditor.py +++ b/src/bk-user/bkuser/biz/auditor.py @@ -167,7 +167,6 @@ def record(self, tenant_user: TenantUser, data_source_user: DataSourceUser): 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() ] @@ -206,14 +205,12 @@ def record(self, tenant_user: TenantUser, data_source_user: DataSourceUser): 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, - data_before={}, data_after={ "department_ids": list( DataSourceDepartmentUserRelation.objects.filter( @@ -229,7 +226,6 @@ def record(self, tenant_user: TenantUser, data_source_user: DataSourceUser): 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), ), ] @@ -256,11 +252,11 @@ def __init__(self, operator: str, tenant_id: str, data_source_user_ids: List[int def pre_record_data_before(self): """记录变更前的相关数据记录""" # 获取用户与部门之间的映射关系 - data_before_map = self.get_user_department_map(self.data_source_user_ids) + data_before_user_dept_map = self.get_user_department_map(self.data_source_user_ids) # 初始化 data_before, 记录变更前用户与部门之间的映射关系 for data_source_user_id in self.data_source_user_ids: - self.data_before[data_source_user_id] = {"department_ids": data_before_map.get(data_source_user_id, [])} + self.data_before[data_source_user_id] = {"department_ids": data_before_user_dept_map[data_source_user_id]} def record(self, data_source_user: DataSourceUser, data_before: Dict, data_after: Dict, extras: Dict): """调用 apps.audit 模块里的方法进行记录""" @@ -282,11 +278,11 @@ def batch_record(self, extras: Dict[str, List] | None = None): id__in=self.data_source_user_ids, ) # 记录变更后的用户与部门之间的映射关系 - data_after_map = self.get_user_department_map(self.data_source_user_ids) + data_after_user_dept_map = self.get_user_department_map(self.data_source_user_ids) for data_source_user in data_source_users: data_before = self.data_before[data_source_user.id] - data_after = {"department_ids": data_after_map.get(data_source_user.id, [])} + data_after = {"department_ids": data_after_user_dept_map[data_source_user.id]} self.record(data_source_user, data_before, data_after, extras or {}) batch_add_audit_records(self.operator, self.tenant_id, self.audit_objects) From cfa89589d2b81c64130ef986642741518c71854b Mon Sep 17 00:00:00 2001 From: rolinchen Date: Wed, 27 Nov 2024 12:17:16 +0800 Subject: [PATCH 4/6] feat: adjust operation audit for data source and users --- .../bkuser/apis/web/data_source/views.py | 90 +-- .../apis/web/organization/views/relations.py | 29 +- .../apis/web/organization/views/users.py | 212 ++----- src/bk-user/bkuser/biz/auditor.py | 578 +++++++++++++----- 4 files changed, 530 insertions(+), 379 deletions(-) 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 208ebcbc1..95642322e 100644 --- a/src/bk-user/bkuser/apis/web/data_source/views.py +++ b/src/bk-user/bkuser/apis/web/data_source/views.py @@ -51,9 +51,6 @@ LocalDataSourceImportInputSLZ, ) from bkuser.apis.web.mixins import CurrentUserTenantMixin -from bkuser.apps.audit.constants import ObjectTypeEnum, OperationEnum -from bkuser.apps.audit.data_models 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, @@ -71,6 +68,7 @@ from bkuser.apps.sync.managers import DataSourceSyncManager from bkuser.apps.sync.models import DataSourceSyncTask, TenantSyncTask from bkuser.apps.tenant.models import TenantDepartment, TenantUser +from bkuser.biz.auditor import DataSourceAuditor from bkuser.biz.data_source import DataSourceHandler from bkuser.biz.exporters import DataSourceUserExporter from bkuser.biz.tenant import TenantUserHandler @@ -81,7 +79,6 @@ 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 @@ -188,15 +185,10 @@ def post(self, request, *args, **kwargs): updater=current_user, ) - # 审计记录 - add_audit_record( - operator=current_user, - tenant_id=current_tenant_id, - operation=OperationEnum.CREATE_DATA_SOURCE, - object_type=ObjectTypeEnum.DATA_SOURCE, - object_id=ds.id, - data_after=get_model_dict(ds), - ) + # 【审计】创建数据源审计对象并记录 + auditor = DataSourceAuditor(request.user.username, current_tenant_id, ds) + # 【审计】将审计记录保存至数据库 + auditor.record_create() return Response( DataSourceCreateOutputSLZ(instance={"id": ds.id}).data, @@ -250,8 +242,9 @@ def put(self, request, *args, **kwargs): slz.is_valid(raise_exception=True) data = slz.validated_data - # 【审计】记录变更前数据 - data_before_data_source = get_model_dict(data_source) + # 【审计】创建数据源审计对象,并记录变更前数据 + auditor = DataSourceAuditor(request.user.username, data_source.owner_tenant_id, data_source) + auditor.pre_record_data_before() with transaction.atomic(): data_source.field_mapping = data["field_mapping"] @@ -261,16 +254,8 @@ def put(self, request, *args, **kwargs): # 由于需要替换敏感信息,因此需要独立调用 set_plugin_cfg 方法 data_source.set_plugin_cfg(data["plugin_config"]) - # 审计记录 - add_audit_record( - operator=data_source.updater, - tenant_id=data_source.owner_tenant_id, - operation=OperationEnum.MODIFY_DATA_SOURCE, - object_type=ObjectTypeEnum.DATA_SOURCE, - object_id=data_source.id, - data_before=data_before_data_source, - data_after=get_model_dict(data_source), - ) + # 【审计】将审计记录保存至数据库 + auditor.record_update(data_source) return Response(status=status.HTTP_204_NO_CONTENT) @@ -303,23 +288,9 @@ def delete(self, request, *args, **kwargs): # 待删除的认证源 waiting_delete_idps = Idp.objects.filter(**idp_filters) - # 记录 data_source 删除前数据 - data_source_audit_object = AuditObject( - id=data_source.id, - type=ObjectTypeEnum.DATA_SOURCE, - operation=OperationEnum.DELETE_DATA_SOURCE, - data_before=get_model_dict(data_source), - ) - # 记录 idp 删除前数据 - idp_audit_objects = [ - AuditObject( - id=data_before_idp.id, - type=ObjectTypeEnum.IDP, - operation=OperationEnum.DELETE_IDP, - data_before=get_model_dict(data_before_idp), - ) - for data_before_idp in list(waiting_delete_idps) - ] + # 【审计】创建数据源审计对象,并记录变更前数据 + auditor = DataSourceAuditor(request.user.username, data_source.owner_tenant_id, data_source) + auditor.pre_record_data_before(list(waiting_delete_idps)) with transaction.atomic(): # 删除认证源敏感信息 @@ -338,13 +309,8 @@ def delete(self, request, *args, **kwargs): # 删除数据源 & 关联资源数据 DataSourceHandler.delete_data_source_and_related_resources(data_source) - audit_objects = [data_source_audit_object] + idp_audit_objects - # 审计记录 - batch_add_audit_records( - operator=request.user.username, - tenant_id=self.get_current_tenant_id(), - objects=audit_objects, - ) + # 【审计】将审计记录保存至数据库 + auditor.record_delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -539,15 +505,10 @@ def post(self, request, *args, **kwargs): logger.exception("本地数据源 %s 导入失败", data_source.id) raise error_codes.DATA_SOURCE_IMPORT_FAILED.f(str(e)) - # 审计记录 - add_audit_record( - operator=task.operator, - tenant_id=data_source.owner_tenant_id, - operation=OperationEnum.SYNC_DATA_SOURCE, - object_type=ObjectTypeEnum.DATA_SOURCE, - object_id=data_source.id, - extras={"overwrite": options.overwrite, "incremental": options.incremental, "trigger": options.trigger}, - ) + # 【审计】创建数据源审计对象并记录 + auditor = DataSourceAuditor(request.user.username, data_source.owner_tenant_id, data_source) + # 【审计】将审计记录保存至数据库 + auditor.record_sync(options) return Response( DataSourceImportOrSyncOutputSLZ( @@ -592,15 +553,10 @@ def post(self, request, *args, **kwargs): logger.exception("创建下发数据源 %s 同步任务失败", data_source.id) raise error_codes.DATA_SOURCE_SYNC_TASK_CREATE_FAILED.f(str(e)) - # 审计记录 - add_audit_record( - operator=task.operator, - tenant_id=data_source.owner_tenant_id, - operation=OperationEnum.SYNC_DATA_SOURCE, - object_type=ObjectTypeEnum.DATA_SOURCE, - object_id=data_source.id, - extras={"overwrite": options.overwrite, "incremental": options.incremental, "trigger": options.trigger}, - ) + # 【审计】创建数据源审计对象并记录 + auditor = DataSourceAuditor(request.user.username, data_source.owner_tenant_id, data_source) + # 【审计】将审计记录保存至数据库 + auditor.record_sync(options) return Response( DataSourceImportOrSyncOutputSLZ( 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 054075e09..83ae5148a 100644 --- a/src/bk-user/bkuser/apis/web/organization/views/relations.py +++ b/src/bk-user/bkuser/apis/web/organization/views/relations.py @@ -30,6 +30,7 @@ TenantDeptUserRelationBatchUpdateInputSLZ, ) from bkuser.apis.web.organization.views.mixins import CurrentUserTenantDataSourceMixin +from bkuser.apps.audit.constants import OperationEnum from bkuser.apps.data_source.models import DataSourceDepartmentUserRelation from bkuser.apps.permission.constants import PermAction from bkuser.apps.permission.permissions import perm_class @@ -69,7 +70,10 @@ def post(self, request, *args, **kwargs): # 【审计】创建审计对象并记录变更前的数据 auditor = TenantUserDepartmentRelationsAuditor( - operator=request.user.username, tenant_id=cur_tenant_id, data_source_user_ids=data_source_user_ids + request.user.username, + cur_tenant_id, + data_source_user_ids, + OperationEnum.CREATE_USER_DEPARTMENT, ) auditor.pre_record_data_before() @@ -82,7 +86,7 @@ def post(self, request, *args, **kwargs): DataSourceDepartmentUserRelation.objects.bulk_create(relations, ignore_conflicts=True) # 【审计】将审计记录保存至数据库 - auditor.batch_record() + auditor.record(extras={"department_ids": list(data_source_dept_ids)}) return Response(status=status.HTTP_204_NO_CONTENT) @@ -119,7 +123,10 @@ def put(self, request, *args, **kwargs): # 【审计】创建审计对象并记录变更前的数据 auditor = TenantUserDepartmentRelationsAuditor( - operator=request.user.username, tenant_id=cur_tenant_id, data_source_user_ids=data_source_user_ids + request.user.username, + cur_tenant_id, + data_source_user_ids, + OperationEnum.MODIFY_USER_DEPARTMENT, ) auditor.pre_record_data_before() @@ -135,7 +142,7 @@ def put(self, request, *args, **kwargs): DataSourceDepartmentUserRelation.objects.bulk_create(relations) # 【审计】将审计记录保存至数据库 - auditor.batch_record(extras={"department_ids": list(data_source_dept_ids)}) + auditor.record(extras={"department_ids": list(data_source_dept_ids)}) return Response(status=status.HTTP_204_NO_CONTENT) @@ -168,7 +175,10 @@ def patch(self, request, *args, **kwargs): # 【审计】创建审计对象 auditor = TenantUserDepartmentRelationsAuditor( - operator=request.user.username, tenant_id=cur_tenant_id, data_source_user_ids=data_source_user_ids + request.user.username, + cur_tenant_id, + data_source_user_ids, + OperationEnum.MODIFY_USER_DEPARTMENT, ) auditor.pre_record_data_before() @@ -186,7 +196,7 @@ def patch(self, request, *args, **kwargs): DataSourceDepartmentUserRelation.objects.bulk_create(relations, ignore_conflicts=True) # 【审计】将审计记录保存至数据库 - auditor.batch_record(extras={"department_id": source_data_source_dept.id}) + auditor.record(extras={"department_id": source_data_source_dept.id}) return Response(status=status.HTTP_204_NO_CONTENT) @@ -221,7 +231,10 @@ def delete(self, request, *args, **kwargs): # 【审计】创建审计对象 auditor = TenantUserDepartmentRelationsAuditor( - operator=request.user.username, tenant_id=cur_tenant_id, data_source_user_ids=data_source_user_ids + request.user.username, + cur_tenant_id, + data_source_user_ids, + OperationEnum.DELETE_USER_DEPARTMENT, ) auditor.pre_record_data_before() @@ -230,6 +243,6 @@ def delete(self, request, *args, **kwargs): ).delete() # 【审计】将审计记录保存至数据库 - auditor.batch_record(extras={"department_id": source_data_source_dept.id}) + auditor.record(extras={"department_id": source_data_source_dept.id}) 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 d67b84d0e..f366b588f 100644 --- a/src/bk-user/bkuser/apis/web/organization/views/users.py +++ b/src/bk-user/bkuser/apis/web/organization/views/users.py @@ -58,9 +58,6 @@ TenantUserUpdateInputSLZ, ) from bkuser.apis.web.organization.views.mixins import CurrentUserTenantDataSourceMixin -from bkuser.apps.audit.constants import ObjectTypeEnum, OperationEnum -from bkuser.apps.audit.data_models 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, @@ -82,7 +79,15 @@ TenantUserValidityPeriodConfig, ) from bkuser.apps.tenant.utils import TenantUserIDGenerator, is_username_frozen -from bkuser.biz.auditor import TenantUserCreateAuditor, TenantUserUpdateDestroyAuditor +from bkuser.biz.auditor import ( + TenantUserAccountExpiredAtUpdateAuditor, + TenantUserCreateAuditor, + TenantUserDestroyAuditor, + TenantUserLeaderRelationsUpdateAuditor, + TenantUserPasswordResetAuditor, + TenantUserStatusUpdateAuditor, + TenantUserUpdateAuditor, +) from bkuser.biz.organization import DataSourceUserHandler from bkuser.common.constants import PERMANENT_TIME from bkuser.common.error_codes import error_codes @@ -500,8 +505,8 @@ 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 = TenantUserUpdateAuditor(request.user.username, cur_tenant_id) auditor.pre_record_data_before(tenant_user, data_source_user) with transaction.atomic(): @@ -529,9 +534,8 @@ 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) @@ -548,10 +552,9 @@ def delete(self, request, *args, **kwargs): data_source_user = tenant_user.data_source_user - # 【审计】记录待删除的用户相关信息(数据源用户、部门、上级、租户用户(包括协同租户用户)) - auditor = TenantUserUpdateDestroyAuditor(request.user.username, cur_tenant_id, True) + # 【审计】创建审计对象并记录待删除的用户相关信息(数据源用户、部门、上级、租户用户(包括协同租户用户)) + auditor = TenantUserDestroyAuditor(request.user.username, cur_tenant_id) auditor.pre_record_data_before(tenant_user, data_source_user) - auditor.record(tenant_user, data_source_user) with transaction.atomic(): # 删除用户意味着租户用户 & 数据源用户都删除,前面检查过权限, @@ -562,8 +565,8 @@ def delete(self, request, *args, **kwargs): DataSourceUserLeaderRelation.objects.filter(leader=data_source_user).delete() data_source_user.delete() - # 【审计】保存记录至数据库 - auditor.save_audit_records() + # 【审计】将审计记录保存至数据库 + auditor.record() return Response(status=status.HTTP_204_NO_CONTENT) @@ -594,11 +597,9 @@ def put(self, request, *args, **kwargs): slz.is_valid(raise_exception=True) data = slz.validated_data - # 【审计】记录变更前的租户用户账号有效期与状态 - data_before = { - "account_expired_at": tenant_user.account_expired_at.strftime("%Y-%m-%d %H:%M:%S"), - "status": tenant_user.status, - } + # 【审计】创建审计对象并记录变更前的用户信息 + auditor = TenantUserAccountExpiredAtUpdateAuditor(request.user.username, self.get_current_tenant_id()) + auditor.pre_record_data_before(tenant_user) tenant_user.account_expired_at = data["account_expired_at"] tenant_user.updater = request.user.username @@ -609,19 +610,8 @@ def put(self, request, *args, **kwargs): tenant_user.save(update_fields=["account_expired_at", "status", "updater", "updated_at"]) - # 记录审计 - add_audit_record( - operator=request.user.username, - tenant_id=self.get_current_tenant_id(), - operation=OperationEnum.MODIFY_USER_ACCOUNT_EXPIRED_AT, - object_type=ObjectTypeEnum.TENANT_USER, - object_id=tenant_user.id, - data_before=data_before, - data_after={ - "account_expired_at": tenant_user.account_expired_at.strftime("%Y-%m-%d %H:%M:%S"), - "status": tenant_user.status, - }, - ) + # 【审计】将审计记录保存至数据库 + auditor.record(tenant_user) return Response(status=status.HTTP_204_NO_CONTENT) @@ -708,16 +698,10 @@ def put(self, request, *args, **kwargs): operator=request.user.username, ) - # 记录审计 - add_audit_record( - operator=request.user.username, - tenant_id=self.get_current_tenant_id(), - operation=OperationEnum.MODIFY_USER_PASSWORD, - object_type=ObjectTypeEnum.DATA_SOURCE_USER, - object_id=data_source_user.id, - object_name=data_source_user.username, - extras={"valid_days": plugin_config.password_expire.valid_time}, - ) + # 【审计】创建审计对象 + auditor = TenantUserPasswordResetAuditor(request.user.username, self.get_current_tenant_id()) + # 【审计】将审计记录保存至数据库 + auditor.record(data_source_user, extras={"valid_days": plugin_config.password_expire.valid_time}) # 发送新密码通知到用户 send_reset_password_to_user.delay(data_source_user.id, raw_password) @@ -785,8 +769,9 @@ def get_queryset(self) -> QuerySet[TenantUser]: def put(self, request, *args, **kwargs): tenant_user = self.get_object() - # 【审计】记录变更前的租户用户状态 - data_before_status = {"status": tenant_user.status} + # 【审计】创建审计对象并记录变更前的用户信息 + auditor = TenantUserStatusUpdateAuditor(request.user.username, self.get_current_tenant_id()) + auditor.pre_record_data_before(tenant_user) # 正常 / 过期的租户用户都可以停用 if tenant_user.status in [TenantUserStatus.ENABLED, TenantUserStatus.EXPIRED]: @@ -802,16 +787,8 @@ def put(self, request, *args, **kwargs): tenant_user.updater = request.user.username tenant_user.save(update_fields=["status", "updater", "updated_at"]) - # 记录审计 - add_audit_record( - operator=request.user.username, - tenant_id=self.get_current_tenant_id(), - operation=OperationEnum.MODIFY_USER_STATUS, - object_type=ObjectTypeEnum.TENANT_USER, - object_id=tenant_user.id, - data_before=data_before_status, - data_after={"status": tenant_user.status}, - ) + # 【审计】将审计记录保存至数据库 + auditor.record(tenant_user) return Response(TenantUserStatusUpdateOutputSLZ(tenant_user).data, status=status.HTTP_200_OK) @@ -883,7 +860,8 @@ def post(self, request, *args, **kwargs): # 【审计】记录创建的用户相关信息(数据源用户、用户部门、租户用户(包括协同租户用户)) auditor = TenantUserCreateAuditor(request.user.username, cur_tenant_id) - auditor.batch_record(data_after_tenant_users) + # 【审计】将审计记录保存至数据库 + auditor.record(data_after_tenant_users) # 对新增的用户进行账密信息初始化 & 发送密码通知 initialize_identity_info_and_send_notification.delay(data_source.id) @@ -1004,9 +982,8 @@ def delete(self, request, *args, **kwargs): 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 = TenantUserDestroyAuditor(request.user.username, cur_tenant_id) auditor.batch_pre_record_data_before(data_before_tenant_users) - auditor.batch_record(data_before_tenant_users) with transaction.atomic(): # 删除用户意味着租户用户 & 数据源用户都删除,前面检查过权限, @@ -1022,7 +999,7 @@ def delete(self, request, *args, **kwargs): DataSourceUser.objects.filter(id__in=data_source_user_ids).delete() # 【审计】保存记录至数据库 - auditor.save_audit_records() + auditor.record() return Response(status=status.HTTP_204_NO_CONTENT) @@ -1053,13 +1030,9 @@ def put(self, request, *args, **kwargs): # 【审计】记录变更前的租户用户数据 data_before_tenant_users = list(TenantUser.objects.filter(id__in=data["user_ids"], tenant_id=cur_tenant_id)) - data_before_dict = { - user.id: { - "account_expired_at": user.account_expired_at.strftime("%Y-%m-%d %H:%M:%S"), - "status": user.status, - } - for user in data_before_tenant_users - } + # 【审计】创建审计对象 + auditor = TenantUserAccountExpiredAtUpdateAuditor(request.user.username, cur_tenant_id) + auditor.batch_pre_record_data_before(data_before_tenant_users) with transaction.atomic(): # 根据租户用户当前状态判断,如果是过期状态则转为正常 @@ -1076,27 +1049,8 @@ def put(self, request, *args, **kwargs): # 【审计】记录变更后的租户用户数据 data_after_tenant_users = list(TenantUser.objects.filter(id__in=data["user_ids"], tenant_id=cur_tenant_id)) - # 【审计】批量创建 AuditObject 对象 - audit_objects = [ - AuditObject( - id=user.id, - type=ObjectTypeEnum.TENANT_USER, - operation=OperationEnum.MODIFY_USER_ACCOUNT_EXPIRED_AT, - data_before=data_before_dict[user.id], - data_after={ - "account_expired_at": user.account_expired_at.strftime("%Y-%m-%d %H:%M:%S"), - "status": user.status, - }, - ) - for user in data_after_tenant_users - ] - - # 记录审计 - batch_add_audit_records( - operator=request.user.username, - tenant_id=cur_tenant_id, - objects=audit_objects, - ) + # 【审计】将审计记录保存至数据库 + auditor.batch_record(data_after_tenant_users) return Response(status=status.HTTP_204_NO_CONTENT) @@ -1129,7 +1083,9 @@ def put(self, request, *args, **kwargs): now = timezone.now() updater = request.user.username - data_before_dict = {user.id: user.status for user in tenant_users} + # 【审计】创建审计对象并记录变更前的租户用户数据 + auditor = TenantUserStatusUpdateAuditor(request.user.username, cur_tenant_id) + auditor.batch_pre_record_data_before(tenant_users) # 停用的时候,正常 / 过期的租户用户都直接停用 if data["status"] == TenantUserStatus.DISABLED: @@ -1157,24 +1113,8 @@ def put(self, request, *args, **kwargs): # 【审计】记录变更后的租户用户数据 data_after_tenant_users = TenantUser.objects.filter(id__in=data["user_ids"], tenant_id=cur_tenant_id) - # 【审计】批量创建 AuditObject 对象 - audit_objects = [ - AuditObject( - id=user.id, - type=ObjectTypeEnum.TENANT_USER, - operation=OperationEnum.MODIFY_USER_STATUS, - data_before={"status": data_before_dict[user.id]}, - data_after={"status": user.status}, - ) - for user in data_after_tenant_users - ] - - # 记录审计 - batch_add_audit_records( - operator=request.user.username, - tenant_id=cur_tenant_id, - objects=audit_objects, - ) + # 【审计】将审计记录保存至数据库 + auditor.batch_record(data_after_tenant_users) return Response(status=status.HTTP_204_NO_CONTENT) @@ -1216,8 +1156,9 @@ def put(self, request, *args, **kwargs): for leader_id, user_id in itertools.product(leader_ids, data_source_user_ids) ] - # 【审计】记录变更前的用户 - 上级映射 - data_before_user_leader_map = self.get_user_leader_map(data_source_user_ids=data_source_user_ids) + # 【审计】创建审计对象 + auditor = TenantUserLeaderRelationsUpdateAuditor(request.user.username, cur_tenant_id, data_source_user_ids) + auditor.pre_record_data_before() with transaction.atomic(): # 先删除现有的用户 - 上级关系 @@ -1226,44 +1167,11 @@ def put(self, request, *args, **kwargs): # 再添加新的用户 - 上级关系 DataSourceUserLeaderRelation.objects.bulk_create(relations) - # 【审计】批量创建 AuditObject 对象 - audit_objects = [ - AuditObject( - id=data_source_user.id, - type=ObjectTypeEnum.DATA_SOURCE_USER, - name=data_source_user.username, - operation=OperationEnum.MODIFY_USER_LEADER, - data_before={"leader_ids": data_before_user_leader_map[data_source_user.id]}, - data_after={"leader_ids": list(leader_ids)}, - ) - for data_source_user in DataSourceUser.objects.filter( - id__in=data_source_user_ids, - ) - ] - - # 记录审计 - batch_add_audit_records( - operator=request.user.username, - tenant_id=cur_tenant_id, - objects=audit_objects, - ) + # 【审计】将审计记录保存至数据库 + auditor.record() return Response(status=status.HTTP_204_NO_CONTENT) - @staticmethod - def get_user_leader_map(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 - class TenantUserPasswordBatchResetApi( CurrentUserTenantDataSourceMixin, ExcludePatchAPIViewMixin, generics.UpdateAPIView @@ -1321,24 +1229,10 @@ def put(self, request, *args, **kwargs): for data_source_user in data_source_users: send_reset_password_to_user.delay(data_source_user.id, raw_password) - # 【审计】批量创建 AuditObject 对象 - audit_objects = [ - AuditObject( - id=user.id, - type=ObjectTypeEnum.DATA_SOURCE_USER, - operation=OperationEnum.MODIFY_USER_PASSWORD, - name=user.username, - extras={"valid_days": plugin_config.password_expire.valid_time}, - ) - for user in data_source_users - ] - - # 记录审计 - batch_add_audit_records( - operator=request.user.username, - tenant_id=cur_tenant_id, - objects=audit_objects, - ) + # 【审计】创建审计对象 + auditor = TenantUserPasswordResetAuditor(request.user.username, self.get_current_tenant_id()) + # 【审计】将审计记录保存至数据库 + auditor.batch_record(data_source_users, extras={"valid_days": plugin_config.password_expire.valid_time}) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/src/bk-user/bkuser/biz/auditor.py b/src/bk-user/bkuser/biz/auditor.py index 3aadbb66b..e7fd88add 100644 --- a/src/bk-user/bkuser/biz/auditor.py +++ b/src/bk-user/bkuser/biz/auditor.py @@ -15,42 +15,114 @@ # 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 typing import Any, Dict, List from bkuser.apps.audit.constants import ObjectTypeEnum, OperationEnum from bkuser.apps.audit.data_models import AuditObject -from bkuser.apps.audit.recorder import batch_add_audit_records +from bkuser.apps.audit.recorder import add_audit_record, batch_add_audit_records from bkuser.apps.data_source.models import ( + DataSource, DataSourceDepartmentUserRelation, DataSourceUser, DataSourceUserLeaderRelation, ) +from bkuser.apps.idp.models import Idp +from bkuser.apps.sync.data_models import DataSourceSyncOptions from bkuser.apps.tenant.models import TenantUser from bkuser.utils.django import get_model_dict -class TenantUserUpdateDestroyAuditor: - """用于记录租户用户修改与删除的审计""" +class DataSourceAuditor: + """用于记录数据源相关操作的审计""" - def __init__(self, operator: str, tenant_id: str, is_delete: bool = False): + def __init__(self, operator: str, tenant_id: str, data_source: DataSource): self.operator = operator self.tenant_id = tenant_id - self.is_delete = is_delete + self.data_source = data_source + self.data_before: Dict[str, Any] = {} - self.data_befores: Dict[str, Dict] = {} + def pre_record_data_before(self, waiting_delete_idps: List[Idp] | None = None): + """记录变更前的相关数据记录""" + self.data_before["data_source"] = get_model_dict(self.data_source) + self.data_before["idps"] = [get_model_dict(idp) for idp in (waiting_delete_idps or [])] + + def record_create(self): + """记录数据源创建操作""" + add_audit_record( + operator=self.operator, + tenant_id=self.tenant_id, + operation=OperationEnum.CREATE_DATA_SOURCE, + object_type=ObjectTypeEnum.DATA_SOURCE, + object_id=self.data_source.id, + data_after=get_model_dict(self.data_source), + ) + + def record_update(self, data_source: DataSource): + """记录数据源更新操作""" + add_audit_record( + operator=self.operator, + tenant_id=self.tenant_id, + operation=OperationEnum.MODIFY_DATA_SOURCE, + object_type=ObjectTypeEnum.DATA_SOURCE, + object_id=self.data_source.id, + data_before=self.data_before["data_source"], + data_after=get_model_dict(data_source), + ) + + def record_delete(self): + """记录数据源删除操作""" + data_source_audit_object = AuditObject( + id=self.data_before["data_source"]["id"], + type=ObjectTypeEnum.DATA_SOURCE, + operation=OperationEnum.DELETE_DATA_SOURCE, + data_before=self.data_before["data_source"], + ) + # 记录 idp 删除前数据 + idp_audit_objects = [ + AuditObject( + id=data_before_idp["id"], + type=ObjectTypeEnum.IDP, + operation=OperationEnum.DELETE_IDP, + data_before=data_before_idp, + ) + for data_before_idp in self.data_before["idps"] + ] + + batch_add_audit_records( + operator=self.operator, + tenant_id=self.tenant_id, + objects=[data_source_audit_object] + idp_audit_objects, + ) + + def record_sync(self, options: DataSourceSyncOptions): + """记录数据源同步操作""" + add_audit_record( + operator=self.operator, + tenant_id=self.tenant_id, + operation=OperationEnum.SYNC_DATA_SOURCE, + object_type=ObjectTypeEnum.DATA_SOURCE, + object_id=self.data_source.id, + extras={"overwrite": options.overwrite, "incremental": options.incremental, "trigger": options.trigger}, + ) + + +class TenantUserUpdateAuditor: + """用于记录租户用户修改的审计""" + + def __init__(self, operator: str, tenant_id: str): + self.operator = operator + self.tenant_id = tenant_id + + self.data_befores: Dict[str, Any] = {} 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] = { + self.data_befores = { "tenant_user": get_model_dict(tenant_user), "data_source_user": get_model_dict(data_source_user), - "collaboration_tenant_users": {}, # 记录修改前的用户部门 "department_ids": list( DataSourceDepartmentUserRelation.objects.filter( @@ -63,49 +135,11 @@ def pre_record_data_before(self, tenant_user: TenantUser, data_source_user: Data ), } - # 记录修改前的协同租户用户 - 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 - } - - def batch_pre_record_data_before(self, tenant_users: List[TenantUser]): - """批量记录变更前的相关数据记录""" - - for tenant_user in tenant_users: - self.pre_record_data_before(tenant_user, 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: OperationEnum.DELETE_USER_DEPARTMENT, - OperationEnum.MODIFY_USER_LEADER: OperationEnum.DELETE_USER_LEADER, - 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 模块里的方法进行记录""" - tenant_user_id = tenant_user.id - - 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_id = data_source_user.id + ds_user_name = data_source_user.username ds_user_object = {"id": ds_user_id, "name": ds_user_name, "type": ObjectTypeEnum.DATA_SOURCE_USER} self.audit_objects.extend( @@ -113,75 +147,148 @@ def record(self, tenant_user: TenantUser, data_source_user: DataSourceUser): # 数据源用户本身信息 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 {}, + operation=OperationEnum.MODIFY_DATA_SOURCE_USER, + data_before=self.data_befores["data_source_user"], + data_after=get_model_dict(data_source_user), ), # 数据源用户的部门 AuditObject( **ds_user_object, - operation=self.get_current_operation(OperationEnum.MODIFY_USER_DEPARTMENT), - data_before={"department_ids": self.data_befores[tenant_user_id]["department_ids"]}, + operation=OperationEnum.MODIFY_USER_DEPARTMENT, + data_before={"department_ids": self.data_befores["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), - data_before={"leader_ids": self.data_befores[tenant_user_id]["leader_ids"]}, + operation=OperationEnum.MODIFY_USER_LEADER, + data_before={"leader_ids": self.data_befores["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 {}, + operation=OperationEnum.MODIFY_TENANT_USER, + data_before=self.data_befores["tenant_user"], + data_after=get_model_dict(tenant_user), ), ] ) - # 若为删除操作,则需记录删除前的协同租户用户 - 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, - ) - for user_id, user_data in self.data_befores[tenant_user_id]["collaboration_tenant_users"].items() - ] - ) + batch_add_audit_records(self.operator, self.tenant_id, self.audit_objects) - def batch_record(self, tenant_users: List[TenantUser]): - """批量记录""" + +class TenantUserDestroyAuditor: + """用于记录租户用户删除的审计""" + + def __init__(self, operator: str, tenant_id: str): + self.operator = operator + self.tenant_id = tenant_id + + 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), + # 记录修改前的协同租户用户 + # 获取与 data_source_user_id 相关的所有 collab_user,排除当前的 tenant_user + "collaboration_tenant_users": { + collab_user.id: get_model_dict(collab_user) + for collab_user in TenantUser.objects.filter(data_source_user_id=data_source_user.id).exclude( + id=tenant_user.id + ) + }, + # 记录修改前的用户部门 + "department_ids": list( + DataSourceDepartmentUserRelation.objects.filter( + user=data_source_user, + ).values_list("department_id", flat=True) + ), + # 记录修改前的用户上级 + "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: - self.record(tenant_user, tenant_user.data_source_user) + self.pre_record_data_before(tenant_user, tenant_user.data_source_user) - # 由于不确定操作是否为批量,故将底层存储数据库的方法抽象,需单独调用 - def save_audit_records(self): + def record(self): + """组装相关数据,并调用 apps.audit 模块里的方法进行记录""" + for tenant_user_id, data_before in self.data_befores.items(): + ds_user_object = { + "id": data_before["data_source_user"]["id"], + "name": data_before["data_source_user"]["username"], + "type": ObjectTypeEnum.DATA_SOURCE_USER, + } + self.audit_objects.extend(self.generate_audit_objects(data_before, tenant_user_id, ds_user_object)) batch_add_audit_records(self.operator, self.tenant_id, self.audit_objects) + @staticmethod + def generate_audit_objects(data_before, tenant_user_id, ds_user_object): + return [ + # 数据源用户本身信息 + AuditObject( + **ds_user_object, + operation=OperationEnum.DELETE_DATA_SOURCE_USER, + data_before=data_before["data_source_user"], + ), + # 数据源用户的部门 + AuditObject( + **ds_user_object, + operation=OperationEnum.DELETE_USER_DEPARTMENT, + data_before={"department_ids": data_before["department_ids"]}, + data_after={"department_ids": []}, + ), + # 数据源用户的 Leader + AuditObject( + **ds_user_object, + operation=OperationEnum.DELETE_USER_LEADER, + data_before={"leader_ids": data_before["leader_ids"]}, + data_after={"leader_ids": []}, + ), + # 租户用户 + AuditObject( + id=tenant_user_id, + type=ObjectTypeEnum.TENANT_USER, + operation=OperationEnum.DELETE_TENANT_USER, + data_before=data_before["tenant_user"], + ), + ] + [ + # 协同租户用户 + AuditObject( + id=user_id, + type=ObjectTypeEnum.TENANT_USER, + operation=OperationEnum.DELETE_COLLABORATION_TENANT_USER, + data_before=user_data, + ) + for user_id, user_data in data_before["collaboration_tenant_users"].items() + ] + class TenantUserCreateAuditor: """用于记录租户用户创建的审计""" @@ -191,63 +298,62 @@ def __init__(self, operator: str, tenant_id: str): self.tenant_id = tenant_id self.audit_objects: List[AuditObject] = [] - def record(self, tenant_user: TenantUser, data_source_user: DataSourceUser): + def record(self, tenant_users: List[TenantUser]): """组装相关数据,并调用 apps.audit 模块里的方法进行记录""" - ds_user_object = { - "id": data_source_user.id, - "name": data_source_user.username, - "type": ObjectTypeEnum.DATA_SOURCE_USER, - } + for tenant_user in tenant_users: + data_source_user = tenant_user.data_source_user - self.audit_objects.extend( - [ - # 数据源用户本身信息 - AuditObject( - **ds_user_object, - operation=OperationEnum.CREATE_DATA_SOURCE_USER, - data_after=get_model_dict(data_source_user), - ), - # 数据源用户的部门 - AuditObject( - **ds_user_object, - operation=OperationEnum.CREATE_USER_DEPARTMENT, - 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_after=get_model_dict(tenant_user), - ), - ] - ) + ds_user_object = { + "id": data_source_user.id, + "name": data_source_user.username, + "type": ObjectTypeEnum.DATA_SOURCE_USER, + } - def batch_record(self, tenant_users: List[TenantUser]): - """批量记录""" + self.audit_objects.extend( + [ + # 数据源用户本身信息 + AuditObject( + **ds_user_object, + operation=OperationEnum.CREATE_DATA_SOURCE_USER, + data_after=get_model_dict(data_source_user), + ), + # 数据源用户的部门 + AuditObject( + **ds_user_object, + operation=OperationEnum.CREATE_USER_DEPARTMENT, + 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_after=get_model_dict(tenant_user), + ), + ] + ) - 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) class TenantUserDepartmentRelationsAuditor: """用于记录用户-部门关系变更的审计""" - def __init__(self, operator: str, tenant_id: str, data_source_user_ids: List[int]): + def __init__(self, operator: str, tenant_id: str, data_source_user_ids: List[int], operation: OperationEnum): self.operator = operator self.tenant_id = tenant_id self.audit_objects: List[AuditObject] = [] - self.data_before: Dict[int, Dict] = {} + self.data_befores: Dict[int, Dict] = {} self.data_source_user_ids = data_source_user_ids + self.operation = operation def pre_record_data_before(self): """记录变更前的相关数据记录""" @@ -256,23 +362,9 @@ def pre_record_data_before(self): # 初始化 data_before, 记录变更前用户与部门之间的映射关系 for data_source_user_id in self.data_source_user_ids: - self.data_before[data_source_user_id] = {"department_ids": data_before_user_dept_map[data_source_user_id]} + self.data_befores[data_source_user_id] = {"department_ids": data_before_user_dept_map[data_source_user_id]} - def record(self, data_source_user: DataSourceUser, data_before: Dict, data_after: Dict, extras: Dict): - """调用 apps.audit 模块里的方法进行记录""" - self.audit_objects.append( - AuditObject( - id=data_source_user.id, - name=data_source_user.username, - type=ObjectTypeEnum.DATA_SOURCE_USER, - operation=OperationEnum.MODIFY_USER_DEPARTMENT, - data_before=data_before, - data_after=data_after, - extras=extras, - ) - ) - - def batch_record(self, extras: Dict[str, List] | None = None): + def record(self, extras: Dict[str, List] | None = None): """批量记录""" data_source_users = DataSourceUser.objects.filter( id__in=self.data_source_user_ids, @@ -281,9 +373,19 @@ def batch_record(self, extras: Dict[str, List] | None = None): data_after_user_dept_map = self.get_user_department_map(self.data_source_user_ids) for data_source_user in data_source_users: - data_before = self.data_before[data_source_user.id] + data_before = self.data_befores[data_source_user.id] data_after = {"department_ids": data_after_user_dept_map[data_source_user.id]} - self.record(data_source_user, data_before, data_after, extras or {}) + self.audit_objects.append( + AuditObject( + id=data_source_user.id, + name=data_source_user.username, + type=ObjectTypeEnum.DATA_SOURCE_USER, + operation=self.operation, + data_before=data_before, + data_after=data_after, + extras=extras or {}, + ) + ) batch_add_audit_records(self.operator, self.tenant_id, self.audit_objects) @staticmethod @@ -299,3 +401,189 @@ def get_user_department_map(data_source_user_ids: List[int]) -> Dict: user_department_map[relation["user_id"]].append(relation["department_id"]) return user_department_map + + +class TenantUserLeaderRelationsUpdateAuditor: + """用于记录用户-上级关系变更的审计""" + + def __init__(self, operator: str, tenant_id: str, data_source_user_ids: List[int]): + self.operator = operator + self.tenant_id = tenant_id + self.audit_objects: List[AuditObject] = [] + self.data_befores: Dict[int, Dict] = {} + self.data_source_user_ids = data_source_user_ids + + def pre_record_data_before(self): + """记录变更前的相关数据记录""" + # 获取用户与上级之间的映射关系 + data_before_user_leader_map = self.get_user_leader_map(self.data_source_user_ids) + + # 初始化 data_before, 记录变更前用户与上级之间的映射关系 + for data_source_user_id in self.data_source_user_ids: + self.data_befores[data_source_user_id] = {"leader_ids": data_before_user_leader_map[data_source_user_id]} + + def record(self, extras: Dict[str, List] | None = None): + """批量记录""" + data_source_users = DataSourceUser.objects.filter( + id__in=self.data_source_user_ids, + ) + # 记录变更后的用户与上级之间的映射关系 + data_after_user_leader_map = self.get_user_leader_map(self.data_source_user_ids) + + for data_source_user in data_source_users: + data_before = self.data_befores[data_source_user.id] + data_after = {"leader_ids": data_after_user_leader_map[data_source_user.id]} + self.audit_objects.append( + AuditObject( + id=data_source_user.id, + name=data_source_user.username, + type=ObjectTypeEnum.DATA_SOURCE_USER, + operation=OperationEnum.MODIFY_USER_LEADER, + data_before=data_before, + data_after=data_after, + extras=extras or {}, + ) + ) + batch_add_audit_records(self.operator, self.tenant_id, self.audit_objects) + + @staticmethod + def get_user_leader_map(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 + + +class TenantUserAccountExpiredAtUpdateAuditor: + """用于记录租户用户账号有效期修改的审计""" + + def __init__(self, operator: str, tenant_id: str): + self.operator = operator + self.tenant_id = tenant_id + self.audit_objects: List[AuditObject] = [] + self.data_befores: Dict[str, Dict] = {} + + def pre_record_data_before(self, tenant_user: TenantUser): + self.data_befores[tenant_user.id] = { + "account_expired_at": tenant_user.account_expired_at.strftime("%Y-%m-%d %H:%M:%S"), + "status": tenant_user.status, + } + + def batch_pre_record_data_before(self, tenant_users: List[TenantUser]): + for tenant_user in tenant_users: + self.pre_record_data_before(tenant_user) + + def record(self, tenant_user: TenantUser): + # 重新获取 tenant_user 数据 + tenant_user.refresh_from_db() + + add_audit_record( + operator=self.operator, + tenant_id=self.tenant_id, + operation=OperationEnum.MODIFY_USER_ACCOUNT_EXPIRED_AT, + object_type=ObjectTypeEnum.DATA_SOURCE_USER, + object_id=tenant_user.data_source_user.id, + data_before=self.data_befores[tenant_user.id], + data_after={ + "account_expired_at": tenant_user.account_expired_at.strftime("%Y-%m-%d %H:%M:%S"), + "status": tenant_user.status, + }, + ) + + def batch_record(self, tenant_users: List[TenantUser]): + for tenant_user in tenant_users: + self.audit_objects.append( + AuditObject( + id=tenant_user.data_source_user.id, + name=tenant_user.data_source_user.username, + type=ObjectTypeEnum.DATA_SOURCE_USER, + operation=OperationEnum.MODIFY_USER_ACCOUNT_EXPIRED_AT, + data_before=self.data_befores[tenant_user.id], + data_after={ + "account_expired_at": tenant_user.account_expired_at.strftime("%Y-%m-%d %H:%M:%S"), + "status": tenant_user.status, + }, + ) + ) + batch_add_audit_records(self.operator, self.tenant_id, self.audit_objects) + + +class TenantUserStatusUpdateAuditor: + """用于记录租户用户账号状态修改的审计""" + + def __init__(self, operator: str, tenant_id: str): + self.operator = operator + self.tenant_id = tenant_id + self.audit_objects: List[AuditObject] = [] + self.data_befores: Dict[str, Dict] = {} + + def pre_record_data_before(self, tenant_user: TenantUser): + self.data_befores[tenant_user.id] = {"status": tenant_user.status} + + def batch_pre_record_data_before(self, tenant_users: List[TenantUser]): + for tenant_user in tenant_users: + self.pre_record_data_before(tenant_user) + + def record(self, tenant_user: TenantUser): + add_audit_record( + operator=self.operator, + tenant_id=self.tenant_id, + operation=OperationEnum.MODIFY_USER_STATUS, + object_type=ObjectTypeEnum.DATA_SOURCE_USER, + object_id=tenant_user.data_source_user.id, + data_before=self.data_befores[tenant_user.id], + data_after={"status": tenant_user.status}, + ) + + def batch_record(self, tenant_users: List[TenantUser]): + for tenant_user in tenant_users: + self.audit_objects.append( + AuditObject( + id=tenant_user.data_source_user.id, + name=tenant_user.data_source_user.username, + type=ObjectTypeEnum.DATA_SOURCE_USER, + operation=OperationEnum.MODIFY_USER_STATUS, + data_before=self.data_befores[tenant_user.id], + data_after={"status": tenant_user.status}, + ) + ) + batch_add_audit_records(self.operator, self.tenant_id, self.audit_objects) + + +class TenantUserPasswordResetAuditor: + """用于记录租户用户密码重置的审计""" + + def __init__(self, operator: str, tenant_id: str): + self.operator = operator + self.tenant_id = tenant_id + self.audit_objects: List[AuditObject] = [] + + def record(self, data_source_user: DataSourceUser, extras: Dict[str, int]): + add_audit_record( + operator=self.operator, + tenant_id=self.tenant_id, + operation=OperationEnum.MODIFY_USER_PASSWORD, + object_type=ObjectTypeEnum.DATA_SOURCE_USER, + object_id=data_source_user.id, + extras=extras, + ) + + def batch_record(self, data_source_users: List[DataSourceUser], extras: Dict[str, int]): + for data_source_user in data_source_users: + self.audit_objects.append( + AuditObject( + id=data_source_user.id, + type=ObjectTypeEnum.DATA_SOURCE_USER, + name=data_source_user.username, + operation=OperationEnum.MODIFY_USER_PASSWORD, + extras=extras, + ) + ) + batch_add_audit_records(self.operator, self.tenant_id, self.audit_objects) From 53d5b31a3d66bdfdad06d9c7d734cd86c630a6d4 Mon Sep 17 00:00:00 2001 From: rolinchen Date: Wed, 27 Nov 2024 15:38:54 +0800 Subject: [PATCH 5/6] feat: adjust operation audit for data source and users --- .../apis/web/organization/views/users.py | 5 +- src/bk-user/bkuser/biz/auditor.py | 167 ++++++++++-------- 2 files changed, 95 insertions(+), 77 deletions(-) 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 f366b588f..3c4258468 100644 --- a/src/bk-user/bkuser/apis/web/organization/views/users.py +++ b/src/bk-user/bkuser/apis/web/organization/views/users.py @@ -552,9 +552,12 @@ def delete(self, request, *args, **kwargs): data_source_user = tenant_user.data_source_user + # 【审计】记录待删除的租户用户 + data_before_tenant_users = list(TenantUser.objects.filter(data_source_user=data_source_user)) + # 【审计】创建审计对象并记录待删除的用户相关信息(数据源用户、部门、上级、租户用户(包括协同租户用户)) auditor = TenantUserDestroyAuditor(request.user.username, cur_tenant_id) - auditor.pre_record_data_before(tenant_user, data_source_user) + auditor.batch_pre_record_data_before(data_before_tenant_users) with transaction.atomic(): # 删除用户意味着租户用户 & 数据源用户都删除,前面检查过权限, diff --git a/src/bk-user/bkuser/biz/auditor.py b/src/bk-user/bkuser/biz/auditor.py index e7fd88add..089c6662a 100644 --- a/src/bk-user/bkuser/biz/auditor.py +++ b/src/bk-user/bkuser/biz/auditor.py @@ -39,12 +39,12 @@ def __init__(self, operator: str, tenant_id: str, data_source: DataSource): self.operator = operator self.tenant_id = tenant_id self.data_source = data_source - self.data_before: Dict[str, Any] = {} + self.data_befores: Dict[str, Any] = {} def pre_record_data_before(self, waiting_delete_idps: List[Idp] | None = None): """记录变更前的相关数据记录""" - self.data_before["data_source"] = get_model_dict(self.data_source) - self.data_before["idps"] = [get_model_dict(idp) for idp in (waiting_delete_idps or [])] + self.data_befores["data_source"] = get_model_dict(self.data_source) + self.data_befores["idps"] = [get_model_dict(idp) for idp in (waiting_delete_idps or [])] def record_create(self): """记录数据源创建操作""" @@ -65,17 +65,17 @@ def record_update(self, data_source: DataSource): operation=OperationEnum.MODIFY_DATA_SOURCE, object_type=ObjectTypeEnum.DATA_SOURCE, object_id=self.data_source.id, - data_before=self.data_before["data_source"], + data_before=self.data_befores["data_source"], data_after=get_model_dict(data_source), ) def record_delete(self): """记录数据源删除操作""" data_source_audit_object = AuditObject( - id=self.data_before["data_source"]["id"], + id=self.data_befores["data_source"]["id"], type=ObjectTypeEnum.DATA_SOURCE, operation=OperationEnum.DELETE_DATA_SOURCE, - data_before=self.data_before["data_source"], + data_before=self.data_befores["data_source"], ) # 记录 idp 删除前数据 idp_audit_objects = [ @@ -85,7 +85,7 @@ def record_delete(self): operation=OperationEnum.DELETE_IDP, data_before=data_before_idp, ) - for data_before_idp in self.data_before["idps"] + for data_before_idp in self.data_befores["idps"] ] batch_add_audit_records( @@ -208,28 +208,31 @@ def pre_record_data_before(self, tenant_user: TenantUser, data_source_user: Data 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), - # 记录修改前的协同租户用户 - # 获取与 data_source_user_id 相关的所有 collab_user,排除当前的 tenant_user - "collaboration_tenant_users": { - collab_user.id: get_model_dict(collab_user) - for collab_user in TenantUser.objects.filter(data_source_user_id=data_source_user.id).exclude( - id=tenant_user.id - ) - }, - # 记录修改前的用户部门 - "department_ids": list( - DataSourceDepartmentUserRelation.objects.filter( - user=data_source_user, - ).values_list("department_id", flat=True) - ), - # 记录修改前的用户上级 - "leader_ids": list( - DataSourceUserLeaderRelation.objects.filter(user=data_source_user).values_list("leader_id", flat=True) - ), - } + # 若为本租户下的用户 + if tenant_user.tenant_id == self.tenant_id: + self.data_befores[tenant_user_id] = { + "tenant_user": get_model_dict(tenant_user), + "data_source_user": get_model_dict(data_source_user), + # 记录修改前的用户部门 + "department_ids": list( + DataSourceDepartmentUserRelation.objects.filter( + user=data_source_user, + ).values_list("department_id", flat=True) + ), + # 记录修改前的用户上级 + "leader_ids": list( + DataSourceUserLeaderRelation.objects.filter(user=data_source_user).values_list( + "leader_id", flat=True + ) + ), + "tenant_id": tenant_user.tenant_id, + } + # 若为协同租户下的用户 + else: + self.data_befores[tenant_user_id] = { + "tenant_user": get_model_dict(tenant_user), + "tenant_id": tenant_user.tenant_id, + } def batch_pre_record_data_before(self, tenant_users: List[TenantUser]): """批量记录变更前的相关数据记录""" @@ -239,36 +242,47 @@ def batch_pre_record_data_before(self, tenant_users: List[TenantUser]): def record(self): """组装相关数据,并调用 apps.audit 模块里的方法进行记录""" - for tenant_user_id, data_before in self.data_befores.items(): - ds_user_object = { - "id": data_before["data_source_user"]["id"], - "name": data_before["data_source_user"]["username"], - "type": ObjectTypeEnum.DATA_SOURCE_USER, - } - self.audit_objects.extend(self.generate_audit_objects(data_before, tenant_user_id, ds_user_object)) + for tenant_user_id, data_befores in self.data_befores.items(): + if data_befores["tenant_id"] == self.tenant_id: + ds_user_object = { + "id": data_befores["data_source_user"]["id"], + "name": data_befores["data_source_user"]["username"], + "type": ObjectTypeEnum.DATA_SOURCE_USER, + } + self.audit_objects.extend(self.generate_audit_objects(data_befores, tenant_user_id, ds_user_object)) + else: + self.audit_objects.append( + # 协同租户用户 + AuditObject( + id=tenant_user_id, + type=ObjectTypeEnum.TENANT_USER, + operation=OperationEnum.DELETE_COLLABORATION_TENANT_USER, + data_before=data_befores["tenant_user"], + ) + ) batch_add_audit_records(self.operator, self.tenant_id, self.audit_objects) @staticmethod - def generate_audit_objects(data_before, tenant_user_id, ds_user_object): + def generate_audit_objects(data_befores, tenant_user_id, ds_user_object): return [ # 数据源用户本身信息 AuditObject( **ds_user_object, operation=OperationEnum.DELETE_DATA_SOURCE_USER, - data_before=data_before["data_source_user"], + data_before=data_befores["data_source_user"], ), # 数据源用户的部门 AuditObject( **ds_user_object, operation=OperationEnum.DELETE_USER_DEPARTMENT, - data_before={"department_ids": data_before["department_ids"]}, + data_before={"department_ids": data_befores["department_ids"]}, data_after={"department_ids": []}, ), # 数据源用户的 Leader AuditObject( **ds_user_object, operation=OperationEnum.DELETE_USER_LEADER, - data_before={"leader_ids": data_before["leader_ids"]}, + data_before={"leader_ids": data_befores["leader_ids"]}, data_after={"leader_ids": []}, ), # 租户用户 @@ -276,17 +290,8 @@ def generate_audit_objects(data_before, tenant_user_id, ds_user_object): id=tenant_user_id, type=ObjectTypeEnum.TENANT_USER, operation=OperationEnum.DELETE_TENANT_USER, - data_before=data_before["tenant_user"], + data_before=data_befores["tenant_user"], ), - ] + [ - # 协同租户用户 - AuditObject( - id=user_id, - type=ObjectTypeEnum.TENANT_USER, - operation=OperationEnum.DELETE_COLLABORATION_TENANT_USER, - data_before=user_data, - ) - for user_id, user_data in data_before["collaboration_tenant_users"].items() ] @@ -309,37 +314,47 @@ def record(self, tenant_users: List[TenantUser]): "type": ObjectTypeEnum.DATA_SOURCE_USER, } - self.audit_objects.extend( - [ - # 数据源用户本身信息 - AuditObject( - **ds_user_object, - operation=OperationEnum.CREATE_DATA_SOURCE_USER, - data_after=get_model_dict(data_source_user), - ), - # 数据源用户的部门 - AuditObject( - **ds_user_object, - operation=OperationEnum.CREATE_USER_DEPARTMENT, - data_after={ - "department_ids": list( - DataSourceDepartmentUserRelation.objects.filter( - user=data_source_user, - ).values_list("department_id", flat=True) - ) - }, - ), - # 租户用户(包含协同租户用户) + # 若为本租户下的用户 + if tenant_user.tenant_id == self.tenant_id: + self.audit_objects.extend( + [ + # 数据源用户本身信息 + AuditObject( + **ds_user_object, + operation=OperationEnum.CREATE_DATA_SOURCE_USER, + data_after=get_model_dict(data_source_user), + ), + # 数据源用户的部门 + AuditObject( + **ds_user_object, + operation=OperationEnum.CREATE_USER_DEPARTMENT, + 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_TENANT_USER, + data_after=get_model_dict(tenant_user), + ), + ] + ) + else: + self.audit_objects.append( + # 协同租户用户信息 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, + operation=OperationEnum.CREATE_COLLABORATION_TENANT_USER, data_after=get_model_dict(tenant_user), ), - ] - ) + ) batch_add_audit_records(self.operator, self.tenant_id, self.audit_objects) From e4c6b9e1924380b701ffe0e59715fb2915c3913f Mon Sep 17 00:00:00 2001 From: rolinchen Date: Thu, 28 Nov 2024 00:36:11 +0800 Subject: [PATCH 6/6] feat: added operation audit for data source and users --- src/bk-user/bkuser/biz/auditor.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/bk-user/bkuser/biz/auditor.py b/src/bk-user/bkuser/biz/auditor.py index 089c6662a..018460b4a 100644 --- a/src/bk-user/bkuser/biz/auditor.py +++ b/src/bk-user/bkuser/biz/auditor.py @@ -243,6 +243,7 @@ def batch_pre_record_data_before(self, tenant_users: List[TenantUser]): def record(self): """组装相关数据,并调用 apps.audit 模块里的方法进行记录""" for tenant_user_id, data_befores in self.data_befores.items(): + # 若为本租户下的用户 if data_befores["tenant_id"] == self.tenant_id: ds_user_object = { "id": data_befores["data_source_user"]["id"], @@ -250,6 +251,7 @@ def record(self): "type": ObjectTypeEnum.DATA_SOURCE_USER, } self.audit_objects.extend(self.generate_audit_objects(data_befores, tenant_user_id, ds_user_object)) + # 若为协同租户下的用户 else: self.audit_objects.append( # 协同租户用户 @@ -306,16 +308,15 @@ def __init__(self, operator: str, tenant_id: str): def record(self, tenant_users: List[TenantUser]): """组装相关数据,并调用 apps.audit 模块里的方法进行记录""" for tenant_user in tenant_users: - data_source_user = tenant_user.data_source_user - - ds_user_object = { - "id": data_source_user.id, - "name": data_source_user.username, - "type": ObjectTypeEnum.DATA_SOURCE_USER, - } - # 若为本租户下的用户 if tenant_user.tenant_id == self.tenant_id: + data_source_user = tenant_user.data_source_user + ds_user_object = { + "id": data_source_user.id, + "name": data_source_user.username, + "type": ObjectTypeEnum.DATA_SOURCE_USER, + } + self.audit_objects.extend( [ # 数据源用户本身信息 @@ -345,6 +346,7 @@ def record(self, tenant_users: List[TenantUser]): ), ] ) + # 若为协同租户下的用户 else: self.audit_objects.append( # 协同租户用户信息