From 1f60e328b6b3c7b03f161269f63f28805eda11a9 Mon Sep 17 00:00:00 2001 From: jiangweidong <1053570670@qq.com> Date: Thu, 3 Apr 2025 14:11:45 +0800 Subject: [PATCH] perf: Export resources to add operation logs --- apps/accounts/mixins.py | 64 +++----------------------- apps/audits/const.py | 1 + apps/audits/utils.py | 18 +++++++- apps/common/drf/renders/base.py | 6 ++- apps/common/drf/renders/mixins.py | 69 ++++++++++++++++++++++++++++ apps/common/views/mixins.py | 28 +---------- apps/terminal/api/session/session.py | 10 ++-- 7 files changed, 105 insertions(+), 91 deletions(-) create mode 100644 apps/common/drf/renders/mixins.py diff --git a/apps/accounts/mixins.py b/apps/accounts/mixins.py index f4bacc2ce..cf921dedc 100644 --- a/apps/accounts/mixins.py +++ b/apps/accounts/mixins.py @@ -1,65 +1,15 @@ from rest_framework.response import Response from rest_framework import status +from django.db.models import Model from django.utils import translation -from django.utils.translation import gettext_noop from audits.const import ActionChoices -from common.views.mixins import RecordViewLogMixin -from common.utils import i18n_fmt +from audits.handler import create_or_update_operate_log -class AccountRecordViewLogMixin(RecordViewLogMixin): +class AccountRecordViewLogMixin(object): get_object: callable - get_queryset: callable - - @staticmethod - def _filter_params(params): - new_params = {} - need_pop_params = ('format', 'order') - for key, value in params.items(): - if key in need_pop_params: - continue - if isinstance(value, list): - value = list(filter(None, value)) - if value: - new_params[key] = value - return new_params - - def get_resource_display(self, request): - query_params = dict(request.query_params) - params = self._filter_params(query_params) - - spm_filter = params.pop("spm", None) - - if not params and not spm_filter: - display_message = gettext_noop("Export all") - elif spm_filter: - display_message = gettext_noop("Export only selected items") - else: - query = ",".join( - ["%s=%s" % (key, value) for key, value in params.items()] - ) - display_message = i18n_fmt(gettext_noop("Export filtered: %s"), query) - return display_message - - @property - def detail_msg(self): - return i18n_fmt( - gettext_noop('User %s view/export secret'), self.request.user - ) - - def list(self, request, *args, **kwargs): - list_func = getattr(super(), 'list') - if not callable(list_func): - return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) - response = list_func(request, *args, **kwargs) - with translation.override('en'): - resource_display = self.get_resource_display(request) - ids = [q.id for q in self.get_queryset()] - self.record_logs( - ids, ActionChoices.view, self.detail_msg, resource_display=resource_display - ) - return response + model: Model def retrieve(self, request, *args, **kwargs): retrieve_func = getattr(super(), 'retrieve') @@ -67,9 +17,9 @@ class AccountRecordViewLogMixin(RecordViewLogMixin): return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) response = retrieve_func(request, *args, **kwargs) with translation.override('en'): - resource = self.get_object() - self.record_logs( - [resource.id], ActionChoices.view, self.detail_msg, resource=resource + create_or_update_operate_log( + ActionChoices.view, self.model._meta.verbose_name, + force=True, resource=self.get_object(), ) return response diff --git a/apps/audits/const.py b/apps/audits/const.py index 43396148f..65db58476 100644 --- a/apps/audits/const.py +++ b/apps/audits/const.py @@ -24,6 +24,7 @@ class ActionChoices(TextChoices): update = "update", _("Update") delete = "delete", _("Delete") create = "create", _("Create") + export = "export", _("Export") # Activities action download = "download", _("Download") connect = "connect", _("Connect") diff --git a/apps/audits/utils.py b/apps/audits/utils.py index 0e8361a0f..4d743da60 100644 --- a/apps/audits/utils.py +++ b/apps/audits/utils.py @@ -6,12 +6,16 @@ from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.db.models import F, Value, CharField from django.db.models.functions import Concat +from django.utils import translation from itertools import chain from common.db.fields import RelatedManager from common.utils import validate_ip, get_ip_city, get_logger from common.utils.timezone import as_current_tz -from .const import DEFAULT_CITY +from .const import DEFAULT_CITY, ActivityChoices as LogChoice +from .handler import create_or_update_operate_log +from .models import ActivityLog + logger = get_logger(__name__) @@ -140,3 +144,15 @@ def construct_userlogin_usernames(user_queryset): ).values_list("usernames_combined_field", flat=True) usernames = list(chain(usernames_original, usernames_combined)) return usernames + + +def record_operate_log_and_activity_log(ids, action, detail, model, **kwargs): + from orgs.utils import current_org + + org_id = current_org.id + with translation.override('en'): + resource_type = model._meta.verbose_name + create_or_update_operate_log(action, resource_type, force=True, **kwargs) + base_data = {'type': LogChoice.operate_log, 'detail': detail, 'org_id': org_id} + activities = [ActivityLog(resource_id=r_id, **base_data) for r_id in ids] + ActivityLog.objects.bulk_create(activities) diff --git a/apps/common/drf/renders/base.py b/apps/common/drf/renders/base.py index db081c3d3..43de3cd91 100644 --- a/apps/common/drf/renders/base.py +++ b/apps/common/drf/renders/base.py @@ -13,11 +13,13 @@ from rest_framework.utils import encoders, json from common.serializers import fields as common_fields from common.utils import get_logger +from .mixins import LogMixin + logger = get_logger(__file__) -class BaseFileRenderer(BaseRenderer): +class BaseFileRenderer(LogMixin, BaseRenderer): # 渲染模板标识, 导入、导出、更新模板: ['import', 'update', 'export'] template = 'export' serializer = None @@ -256,6 +258,8 @@ class BaseFileRenderer(BaseRenderer): logger.debug(e, exc_info=True) value = 'Render error! ({})'.format(self.media_type).encode('utf-8') return value + + self.record_logs(request, view, data) return value def compress_into_zip_file(self, value, request, response): diff --git a/apps/common/drf/renders/mixins.py b/apps/common/drf/renders/mixins.py new file mode 100644 index 000000000..15586d03f --- /dev/null +++ b/apps/common/drf/renders/mixins.py @@ -0,0 +1,69 @@ +from django.utils.translation import gettext_noop + +from audits.const import ActionChoices +from audits.utils import record_operate_log_and_activity_log +from common.utils import get_logger + + +logger = get_logger(__file__) + + +class LogMixin(object): + @staticmethod + def _clean_params(query_params): + clean_params = {} + ignore_params = ('format', 'order') + for key, value in dict(query_params).items(): + if key in ignore_params: + continue + if isinstance(value, list): + value = list(filter(None, value)) + if value: + clean_params[key] = value + return clean_params + + @staticmethod + def _get_model(view): + model = getattr(view, 'model', None) + if not model: + serializer = view.get_serializer() + if serializer: + model = serializer.Meta.model + return model + + @staticmethod + def _build_after(params, data): + base = { + gettext_noop('Resource count'): {'value': len(data)} + } + extra = {key: {'value': value} for key, value in params.items()} + return {**extra, **base} + + @staticmethod + def get_resource_display(params): + spm_filter = params.pop("spm", None) + if not params and not spm_filter: + display_message = gettext_noop("Export all") + elif spm_filter: + display_message = gettext_noop("Export only selected items") + else: + display_message = gettext_noop("Export filtered") + return display_message + + def record_logs(self, request, view, data): + activity_ids, activity_detail = [], '' + model = self._get_model(view) + if not model: + logger.warning('Model is not defined in view: %s' % view) + return + + params = self._clean_params(request.query_params) + resource_display = self.get_resource_display(params) + after = self._build_after(params, data) + if hasattr(view, 'get_activity_detail_msg'): + activity_detail = view.get_activity_detail_msg() + activity_ids = [d['id'] for d in data if 'id' in d] + record_operate_log_and_activity_log( + activity_ids, ActionChoices.export, activity_detail, + model, resource_display=resource_display, after=after + ) diff --git a/apps/common/views/mixins.py b/apps/common/views/mixins.py index 6f59ac21a..21c18515e 100644 --- a/apps/common/views/mixins.py +++ b/apps/common/views/mixins.py @@ -2,20 +2,14 @@ # from django.contrib.auth.mixins import UserPassesTestMixin from django.http.response import JsonResponse -from django.db.models import Model -from django.utils import translation from rest_framework import permissions from rest_framework.request import Request -from audits.const import ActivityChoices -from audits.handler import create_or_update_operate_log -from audits.models import ActivityLog from common.exceptions import UserConfirmRequired -from orgs.utils import current_org + __all__ = [ "PermissionsMixin", - "RecordViewLogMixin", "UserConfirmRequiredExceptionMixin", ] @@ -45,23 +39,3 @@ class PermissionsMixin(UserPassesTestMixin): if not permission_class().has_permission(self.request, self): return False return True - - -class RecordViewLogMixin: - model: Model - - def record_logs(self, ids, action, detail, model=None, **kwargs): - with translation.override('en'): - model = model or self.model - resource_type = model._meta.verbose_name - create_or_update_operate_log( - action, resource_type, force=True, **kwargs - ) - activities = [ - ActivityLog( - resource_id=resource_id, type=ActivityChoices.operate_log, - detail=detail, org_id=current_org.id, - ) - for resource_id in ids - ] - ActivityLog.objects.bulk_create(activities) diff --git a/apps/terminal/api/session/session.py b/apps/terminal/api/session/session.py index 7b4e0bac9..317a7b824 100644 --- a/apps/terminal/api/session/session.py +++ b/apps/terminal/api/session/session.py @@ -18,6 +18,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from audits.const import ActionChoices +from audits.utils import record_operate_log_and_activity_log from common.api import AsyncApiMixin from common.const.http import GET, POST from common.drf.filters import BaseFilterSet @@ -27,7 +28,6 @@ from common.permissions import IsServiceAccount from common.storage.replay import ReplayStorageHandler, SessionPartReplayStorageHandler from common.utils import data_to_json, is_uuid, i18n_fmt from common.utils import get_logger, get_object_or_none -from common.views.mixins import RecordViewLogMixin from orgs.mixins.api import OrgBulkModelViewSet from orgs.utils import tmp_to_root_org, tmp_to_org from rbac.permissions import RBACPermission @@ -77,7 +77,7 @@ class SessionFilterSet(BaseFilterSet): return queryset.filter(terminal__name=value) -class SessionViewSet(RecordViewLogMixin, OrgBulkModelViewSet): +class SessionViewSet(OrgBulkModelViewSet): model = Session serializer_classes = { 'default': serializers.SessionSerializer, @@ -153,7 +153,7 @@ class SessionViewSet(RecordViewLogMixin, OrgBulkModelViewSet): detail = i18n_fmt( REPLAY_OP, self.request.user, _('Download'), str(session) ) - self.record_logs( + record_operate_log_and_activity_log( [session.asset_id], ActionChoices.download, detail, model=Session, resource_display=str(session) ) @@ -211,7 +211,7 @@ class SessionViewSet(RecordViewLogMixin, OrgBulkModelViewSet): return super().perform_create(serializer) -class SessionReplayViewSet(AsyncApiMixin, RecordViewLogMixin, viewsets.ViewSet): +class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet): serializer_class = serializers.ReplaySerializer download_cache_key = "SESSION_REPLAY_DOWNLOAD_{}" session = None @@ -283,7 +283,7 @@ class SessionReplayViewSet(AsyncApiMixin, RecordViewLogMixin, viewsets.ViewSet): detail = i18n_fmt( REPLAY_OP, self.request.user, _('View'), str(session) ) - self.record_logs( + record_operate_log_and_activity_log( [session.asset_id], ActionChoices.download, detail, model=Session, resource_display=str(session) )