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..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,8 +51,6 @@ LocalDataSourceImportInputSLZ, ) 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.data_source.constants import DataSourceTypeEnum from bkuser.apps.data_source.models import ( DataSource, @@ -70,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 @@ -186,19 +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, - extras={ - "plugin_config": ds.plugin_config, - "field_mapping": ds.field_mapping, - "sync_config": ds.sync_config, - }, - ) + # 【审计】创建数据源审计对象并记录 + auditor = DataSourceAuditor(request.user.username, current_tenant_id, ds) + # 【审计】将审计记录保存至数据库 + auditor.record_create() return Response( DataSourceCreateOutputSLZ(instance={"id": ds.id}).data, @@ -252,12 +242,9 @@ def put(self, request, *args, **kwargs): slz.is_valid(raise_exception=True) data = slz.validated_data - # 【审计】记录变更前数据 - data_before = { - "plugin_config": data_source.plugin_config, - "field_mapping": data_source.field_mapping, - "sync_config": data_source.sync_config, - } + # 【审计】创建数据源审计对象,并记录变更前数据 + 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"] @@ -267,15 +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, - extras={"data_before": data_before}, - ) + # 【审计】将审计记录保存至数据库 + auditor.record_update(data_source) return Response(status=status.HTTP_204_NO_CONTENT) @@ -308,14 +288,9 @@ 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_id = data_source.id - plugin_config = data_source.plugin_config - field_mapping = data_source.field_mapping - sync_config = data_source.sync_config + # 【审计】创建数据源审计对象,并记录变更前数据 + auditor = DataSourceAuditor(request.user.username, data_source.owner_tenant_id, data_source) + auditor.pre_record_data_before(list(waiting_delete_idps)) with transaction.atomic(): # 删除认证源敏感信息 @@ -334,21 +309,8 @@ def delete(self, request, *args, **kwargs): # 删除数据源 & 关联资源数据 DataSourceHandler.delete_data_source_and_related_resources(data_source) - # 审计记录 - add_audit_record( - 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, - }, - ) + # 【审计】将审计记录保存至数据库 + auditor.record_delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -543,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( @@ -596,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 37e9152e3..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,10 +30,12 @@ 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 from bkuser.apps.tenant.models import TenantDepartment, TenantUser +from bkuser.biz.auditor import TenantUserDepartmentRelationsAuditor class TenantDeptUserRelationBatchCreateApi(CurrentUserTenantDataSourceMixin, generics.CreateAPIView): @@ -66,6 +68,15 @@ def post(self, request, *args, **kwargs): id__in=data["user_ids"], ).values_list("data_source_user_id", flat=True) + # 【审计】创建审计对象并记录变更前的数据 + auditor = TenantUserDepartmentRelationsAuditor( + request.user.username, + cur_tenant_id, + data_source_user_ids, + OperationEnum.CREATE_USER_DEPARTMENT, + ) + auditor.pre_record_data_before() + # 复制操作:为数据源部门 & 用户添加关联边,但是不会影响存量的关联边 relations = [ DataSourceDepartmentUserRelation(user_id=user_id, department_id=dept_id, data_source=data_source) @@ -74,6 +85,9 @@ def post(self, request, *args, **kwargs): # 由于复制操作不会影响存量的关联边,所以需要忽略冲突,避免出现用户复选的情况 DataSourceDepartmentUserRelation.objects.bulk_create(relations, ignore_conflicts=True) + # 【审计】将审计记录保存至数据库 + auditor.record(extras={"department_ids": list(data_source_dept_ids)}) + return Response(status=status.HTTP_204_NO_CONTENT) @@ -107,6 +121,15 @@ def put(self, request, *args, **kwargs): id__in=data["user_ids"], ).values_list("data_source_user_id", flat=True) + # 【审计】创建审计对象并记录变更前的数据 + auditor = TenantUserDepartmentRelationsAuditor( + request.user.username, + cur_tenant_id, + data_source_user_ids, + OperationEnum.MODIFY_USER_DEPARTMENT, + ) + auditor.pre_record_data_before() + # 移动操作:为数据源部门 & 用户添加关联边,但是会删除这批用户所有的存量关联边 with transaction.atomic(): # 先删除 @@ -118,6 +141,9 @@ def put(self, request, *args, **kwargs): ] DataSourceDepartmentUserRelation.objects.bulk_create(relations) + # 【审计】将审计记录保存至数据库 + auditor.record(extras={"department_ids": list(data_source_dept_ids)}) + return Response(status=status.HTTP_204_NO_CONTENT) @swagger_auto_schema( @@ -147,6 +173,15 @@ def patch(self, request, *args, **kwargs): id__in=data["user_ids"], ).values_list("data_source_user_id", flat=True) + # 【审计】创建审计对象 + auditor = TenantUserDepartmentRelationsAuditor( + request.user.username, + cur_tenant_id, + data_source_user_ids, + OperationEnum.MODIFY_USER_DEPARTMENT, + ) + auditor.pre_record_data_before() + # 移动操作:为数据源部门 & 用户添加关联边,但是会删除这批用户在当前部门的存量关联边 with transaction.atomic(): # 先删除(仅限于指定部门) @@ -160,6 +195,9 @@ def patch(self, request, *args, **kwargs): ] DataSourceDepartmentUserRelation.objects.bulk_create(relations, ignore_conflicts=True) + # 【审计】将审计记录保存至数据库 + auditor.record(extras={"department_id": source_data_source_dept.id}) + return Response(status=status.HTTP_204_NO_CONTENT) @@ -191,8 +229,20 @@ def delete(self, request, *args, **kwargs): id__in=data["user_ids"], ).values_list("data_source_user_id", flat=True) + # 【审计】创建审计对象 + auditor = TenantUserDepartmentRelationsAuditor( + request.user.username, + cur_tenant_id, + data_source_user_ids, + OperationEnum.DELETE_USER_DEPARTMENT, + ) + auditor.pre_record_data_before() + DataSourceDepartmentUserRelation.objects.filter( user_id__in=data_source_user_ids, department=source_data_source_dept ).delete() + # 【审计】将审计记录保存至数据库 + 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 09d33fbf6..3c4258468 100644 --- a/src/bk-user/bkuser/apis/web/organization/views/users.py +++ b/src/bk-user/bkuser/apis/web/organization/views/users.py @@ -79,6 +79,15 @@ TenantUserValidityPeriodConfig, ) from bkuser.apps.tenant.utils import TenantUserIDGenerator, is_username_frozen +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 @@ -496,6 +505,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 = TenantUserUpdateAuditor(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 +534,15 @@ 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) + 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 +551,14 @@ def delete(self, request, *args, **kwargs): raise error_codes.TENANT_USER_DELETE_FAILED.f(_("仅可删除非协同产生的租户用户")) 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.batch_pre_record_data_before(data_before_tenant_users) + with transaction.atomic(): # 删除用户意味着租户用户 & 数据源用户都删除,前面检查过权限, # 因此这里所有协同产生的租户用户也需要删除(不等同步,立即生效) @@ -543,6 +568,9 @@ def delete(self, request, *args, **kwargs): DataSourceUserLeaderRelation.objects.filter(leader=data_source_user).delete() data_source_user.delete() + # 【审计】将审计记录保存至数据库 + auditor.record() + return Response(status=status.HTTP_204_NO_CONTENT) @@ -572,6 +600,10 @@ def put(self, request, *args, **kwargs): slz.is_valid(raise_exception=True) data = slz.validated_data + # 【审计】创建审计对象并记录变更前的用户信息 + 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 @@ -581,6 +613,9 @@ def put(self, request, *args, **kwargs): tenant_user.save(update_fields=["account_expired_at", "status", "updater", "updated_at"]) + # 【审计】将审计记录保存至数据库 + auditor.record(tenant_user) + return Response(status=status.HTTP_204_NO_CONTENT) @@ -666,6 +701,11 @@ def put(self, request, *args, **kwargs): operator=request.user.username, ) + # 【审计】创建审计对象 + 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) return Response(status=status.HTTP_204_NO_CONTENT) @@ -731,6 +771,11 @@ def get_queryset(self) -> QuerySet[TenantUser]: ) def put(self, request, *args, **kwargs): tenant_user = self.get_object() + + # 【审计】创建审计对象并记录变更前的用户信息 + 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]: tenant_user.status = TenantUserStatus.DISABLED @@ -744,6 +789,10 @@ def put(self, request, *args, **kwargs): tenant_user.updater = request.user.username tenant_user.save(update_fields=["status", "updater", "updated_at"]) + + # 【审计】将审计记录保存至数据库 + auditor.record(tenant_user) + return Response(TenantUserStatusUpdateOutputSLZ(tenant_user).data, status=status.HTTP_200_OK) @@ -807,6 +856,16 @@ 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.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 +981,13 @@ def delete(self, request, *args, **kwargs): ).values_list("data_source_user_id", flat=True) ) + # 【审计】记录待删除的租户用户 + data_before_tenant_users = list(TenantUser.objects.filter(data_source_user_id__in=data_source_user_ids)) + + # 【审计】记录待删除的用户相关信息(数据源用户、部门、上级、租户用户(包括协同租户用户)) + auditor = TenantUserDestroyAuditor(request.user.username, cur_tenant_id) + auditor.batch_pre_record_data_before(data_before_tenant_users) + with transaction.atomic(): # 删除用户意味着租户用户 & 数据源用户都删除,前面检查过权限, # 因此这里所有协同产生的租户用户也需要删除(不等同步,立即生效) @@ -935,6 +1001,9 @@ def delete(self, request, *args, **kwargs): # 最后才是批量回收数据源用户 DataSourceUser.objects.filter(id__in=data_source_user_ids).delete() + # 【审计】保存记录至数据库 + auditor.record() + return Response(status=status.HTTP_204_NO_CONTENT) @@ -961,6 +1030,13 @@ 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)) + + # 【审计】创建审计对象 + auditor = TenantUserAccountExpiredAtUpdateAuditor(request.user.username, cur_tenant_id) + auditor.batch_pre_record_data_before(data_before_tenant_users) + with transaction.atomic(): # 根据租户用户当前状态判断,如果是过期状态则转为正常 TenantUser.objects.filter( @@ -973,6 +1049,12 @@ 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)) + + # 【审计】将审计记录保存至数据库 + auditor.batch_record(data_after_tenant_users) + return Response(status=status.HTTP_204_NO_CONTENT) @@ -1004,6 +1086,10 @@ def put(self, request, *args, **kwargs): now = timezone.now() updater = request.user.username + # 【审计】创建审计对象并记录变更前的租户用户数据 + auditor = TenantUserStatusUpdateAuditor(request.user.username, cur_tenant_id) + auditor.batch_pre_record_data_before(tenant_users) + # 停用的时候,正常 / 过期的租户用户都直接停用 if data["status"] == TenantUserStatus.DISABLED: tenant_users.update( @@ -1027,6 +1113,12 @@ 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) + + # 【审计】将审计记录保存至数据库 + auditor.batch_record(data_after_tenant_users) + return Response(status=status.HTTP_204_NO_CONTENT) @@ -1067,6 +1159,10 @@ def put(self, request, *args, **kwargs): for leader_id, user_id in itertools.product(leader_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(): # 先删除现有的用户 - 上级关系 DataSourceUserLeaderRelation.objects.filter(user_id__in=data_source_user_ids).delete() @@ -1074,6 +1170,9 @@ def put(self, request, *args, **kwargs): # 再添加新的用户 - 上级关系 DataSourceUserLeaderRelation.objects.bulk_create(relations) + # 【审计】将审计记录保存至数据库 + auditor.record() + return Response(status=status.HTTP_204_NO_CONTENT) @@ -1133,6 +1232,11 @@ 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) + # 【审计】创建审计对象 + 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/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/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..018460b4a --- /dev/null +++ b/src/bk-user/bkuser/biz/auditor.py @@ -0,0 +1,606 @@ +# -*- 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 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 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 DataSourceAuditor: + """用于记录数据源相关操作的审计""" + + 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_befores: Dict[str, Any] = {} + + def pre_record_data_before(self, waiting_delete_idps: List[Idp] | None = None): + """记录变更前的相关数据记录""" + 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): + """记录数据源创建操作""" + 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_befores["data_source"], + data_after=get_model_dict(data_source), + ) + + def record_delete(self): + """记录数据源删除操作""" + data_source_audit_object = AuditObject( + id=self.data_befores["data_source"]["id"], + type=ObjectTypeEnum.DATA_SOURCE, + operation=OperationEnum.DELETE_DATA_SOURCE, + data_before=self.data_befores["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_befores["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 的审计数据 + self.data_befores = { + "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) + ), + } + + def record(self, tenant_user: TenantUser, data_source_user: DataSourceUser): + """组装相关数据,并调用 apps.audit 模块里的方法进行记录""" + + 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( + [ + # 数据源用户本身信息 + AuditObject( + **ds_user_object, + 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=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) + ) + }, + ), + # 数据源用户的 Leader + AuditObject( + **ds_user_object, + 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 + ) + ) + }, + ), + # 租户用户 + AuditObject( + id=tenant_user.id, + type=ObjectTypeEnum.TENANT_USER, + operation=OperationEnum.MODIFY_TENANT_USER, + data_before=self.data_befores["tenant_user"], + data_after=get_model_dict(tenant_user), + ), + ] + ) + + batch_add_audit_records(self.operator, self.tenant_id, self.audit_objects) + + +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 的审计数据 + # 若为本租户下的用户 + 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]): + """批量记录变更前的相关数据记录""" + + for tenant_user in tenant_users: + self.pre_record_data_before(tenant_user, tenant_user.data_source_user) + + 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"], + "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_befores, tenant_user_id, ds_user_object): + return [ + # 数据源用户本身信息 + AuditObject( + **ds_user_object, + operation=OperationEnum.DELETE_DATA_SOURCE_USER, + data_before=data_befores["data_source_user"], + ), + # 数据源用户的部门 + AuditObject( + **ds_user_object, + operation=OperationEnum.DELETE_USER_DEPARTMENT, + 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_befores["leader_ids"]}, + data_after={"leader_ids": []}, + ), + # 租户用户 + AuditObject( + id=tenant_user_id, + type=ObjectTypeEnum.TENANT_USER, + operation=OperationEnum.DELETE_TENANT_USER, + data_before=data_befores["tenant_user"], + ), + ] + + +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_users: List[TenantUser]): + """组装相关数据,并调用 apps.audit 模块里的方法进行记录""" + for tenant_user in tenant_users: + # 若为本租户下的用户 + 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( + [ + # 数据源用户本身信息 + 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, + data_after=get_model_dict(tenant_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], operation: OperationEnum): + 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 + self.operation = operation + + def pre_record_data_before(self): + """记录变更前的相关数据记录""" + # 获取用户与部门之间的映射关系 + 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_befores[data_source_user_id] = {"department_ids": data_before_user_dept_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_dept_map = self.get_user_department_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 = {"department_ids": data_after_user_dept_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=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 + 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 + + +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) 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))