1
0
mirror of https://github.com/jumpserver/jumpserver.git synced 2025-05-06 15:16:32 +00:00

perf: Export resources to add operation logs

This commit is contained in:
jiangweidong 2025-04-03 14:11:45 +08:00 committed by Bryan
parent e8e0ea920b
commit 1f60e328b6
7 changed files with 105 additions and 91 deletions
apps
accounts
audits
common
terminal/api/session

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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):

View File

@ -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
)

View File

@ -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)

View File

@ -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)
)