diff --git a/apps/accounts/api/account/account.py b/apps/accounts/api/account/account.py index 201e63a20..740ccf041 100644 --- a/apps/accounts/api/account/account.py +++ b/apps/accounts/api/account/account.py @@ -7,10 +7,10 @@ from rest_framework.status import HTTP_200_OK from accounts import serializers from accounts.filters import AccountFilterSet from accounts.models import Account +from accounts.mixins import AccountRecordViewLogMixin from assets.models import Asset, Node from common.api.mixin import ExtraFilterFieldsMixin from common.permissions import UserConfirmation, ConfirmType, IsValidUser -from common.views.mixins import RecordViewLogMixin from orgs.mixins.api import OrgBulkModelViewSet from rbac.permissions import RBACPermission @@ -86,7 +86,7 @@ class AccountViewSet(OrgBulkModelViewSet): return Response(status=HTTP_200_OK) -class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet): +class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet): """ 因为可能要导出所有账号,所以单独建立了一个 viewset """ @@ -115,7 +115,7 @@ class AssetAccountBulkCreateApi(CreateAPIView): return Response(data=serializer.data, status=HTTP_200_OK) -class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, RecordViewLogMixin, ListAPIView): +class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixin, ListAPIView): model = Account.history.model serializer_class = serializers.AccountHistorySerializer http_method_names = ['get', 'options'] diff --git a/apps/accounts/api/account/template.py b/apps/accounts/api/account/template.py index 1d2508764..f9c3637f8 100644 --- a/apps/accounts/api/account/template.py +++ b/apps/accounts/api/account/template.py @@ -4,10 +4,10 @@ from rest_framework.response import Response from accounts import serializers from accounts.models import AccountTemplate +from accounts.mixins import AccountRecordViewLogMixin from assets.const import Protocol from common.drf.filters import BaseFilterSet from common.permissions import UserConfirmation, ConfirmType -from common.views.mixins import RecordViewLogMixin from orgs.mixins.api import OrgBulkModelViewSet from rbac.permissions import RBACPermission @@ -55,7 +55,7 @@ class AccountTemplateViewSet(OrgBulkModelViewSet): return Response(data=serializer.data) -class AccountTemplateSecretsViewSet(RecordViewLogMixin, AccountTemplateViewSet): +class AccountTemplateSecretsViewSet(AccountRecordViewLogMixin, AccountTemplateViewSet): serializer_classes = { 'default': serializers.AccountTemplateSecretSerializer, } diff --git a/apps/accounts/mixins.py b/apps/accounts/mixins.py new file mode 100644 index 000000000..f4bacc2ce --- /dev/null +++ b/apps/accounts/mixins.py @@ -0,0 +1,75 @@ +from rest_framework.response import Response +from rest_framework import status +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 + + +class AccountRecordViewLogMixin(RecordViewLogMixin): + 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 + + def retrieve(self, request, *args, **kwargs): + retrieve_func = getattr(super(), 'retrieve') + if not callable(retrieve_func): + 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 + ) + return response + diff --git a/apps/audits/const.py b/apps/audits/const.py index a2832ef9d..44d3a556f 100644 --- a/apps/audits/const.py +++ b/apps/audits/const.py @@ -25,6 +25,7 @@ class ActionChoices(TextChoices): delete = "delete", _("Delete") create = "create", _("Create") # Activities action + download = "download", _("Download") connect = "connect", _("Connect") login = "login", _("Login") change_auth = "change_password", _("Change password") diff --git a/apps/common/api/patch.py b/apps/common/api/patch.py index f79957546..821dff4ea 100644 --- a/apps/common/api/patch.py +++ b/apps/common/api/patch.py @@ -88,6 +88,7 @@ class AsyncApiMixin(InterceptMixin): if not self.is_need_async(): return handler(*args, **kwargs) resp = self.do_async(handler, *args, **kwargs) + self.async_callback(*args, **kwargs) return resp def is_need_refresh(self): @@ -98,6 +99,9 @@ class AsyncApiMixin(InterceptMixin): def is_need_async(self): return False + def async_callback(self, params): + pass + def do_async(self, handler, *args, **kwargs): data = self.get_cache_data() if not data: diff --git a/apps/common/views/mixins.py b/apps/common/views/mixins.py index 6240fee5a..be9f7347f 100644 --- a/apps/common/views/mixins.py +++ b/apps/common/views/mixins.py @@ -2,16 +2,14 @@ # from django.contrib.auth.mixins import UserPassesTestMixin from django.http.response import JsonResponse -from django.utils import translation -from django.utils.translation import gettext_noop +from django.db.models import Model from rest_framework import permissions from rest_framework.request import Request -from audits.const import ActionChoices, ActivityChoices +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 common.utils import i18n_fmt from orgs.utils import current_org __all__ = [ @@ -49,66 +47,19 @@ class PermissionsMixin(UserPassesTestMixin): class RecordViewLogMixin: - ACTION = ActionChoices.view + model: Model - @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 - - def record_logs(self, ids, **kwargs): - resource_type = self.model._meta.verbose_name + def record_logs(self, ids, action, detail, model=None, **kwargs): + model = model or self.model + resource_type = model._meta.verbose_name create_or_update_operate_log( - self.ACTION, resource_type, force=True, **kwargs - ) - detail = i18n_fmt( - gettext_noop('User %s view/export secret'), self.request.user + action, resource_type, force=True, **kwargs ) activities = [ ActivityLog( - resource_id=getattr(resource_id, 'pk', resource_id), - type=ActivityChoices.operate_log, detail=detail, org_id=current_org.id, + 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) - - def list(self, request, *args, **kwargs): - response = super().list(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, resource_display=resource_display) - return response - - def retrieve(self, request, *args, **kwargs): - response = super().retrieve(request, *args, **kwargs) - with translation.override('en'): - resource = self.get_object() - self.record_logs([resource.id], resource=resource) - return response diff --git a/apps/terminal/api/session/session.py b/apps/terminal/api/session/session.py index 1782b0207..98a1fe73c 100644 --- a/apps/terminal/api/session/session.py +++ b/apps/terminal/api/session/session.py @@ -8,7 +8,7 @@ from django.db.models import F from django.http import FileResponse from django.shortcuts import get_object_or_404, reverse from django.utils.encoding import escape_uri_path -from django.utils.translation import gettext as _ +from django.utils.translation import gettext_noop, gettext as _ from django_filters import rest_framework as filters from rest_framework import generics from rest_framework import viewsets, views @@ -16,14 +16,16 @@ from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from audits.const import ActionChoices from common.api import AsyncApiMixin from common.const.http import GET from common.drf.filters import BaseFilterSet from common.drf.filters import DatetimeRangeFilterBackend from common.drf.renders import PassthroughRenderer from common.storage.replay import ReplayStorageHandler -from common.utils import data_to_json, is_uuid +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 @@ -70,7 +72,7 @@ class SessionFilterSet(BaseFilterSet): return queryset.filter(terminal__name=value) -class SessionViewSet(OrgBulkModelViewSet): +class SessionViewSet(RecordViewLogMixin, OrgBulkModelViewSet): model = Session serializer_classes = { 'default': serializers.SessionSerializer, @@ -132,6 +134,15 @@ class SessionViewSet(OrgBulkModelViewSet): filename = escape_uri_path('{}.tar'.format(storage.obj.id)) disposition = "attachment; filename*=UTF-8''{}".format(filename) response["Content-Disposition"] = disposition + + detail = i18n_fmt( + gettext_noop(f'User %s %s session %s replay'), self.request.user, + gettext_noop(ActionChoices.download), str(storage.obj) + ) + self.record_logs( + [storage.obj.asset_id], ActionChoices.download, detail, + model=Session, resource_display=str(storage.obj) + ) return response def get_queryset(self): @@ -152,7 +163,7 @@ class SessionViewSet(OrgBulkModelViewSet): return super().perform_create(serializer) -class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet): +class SessionReplayViewSet(AsyncApiMixin, RecordViewLogMixin, viewsets.ViewSet): serializer_class = serializers.ReplaySerializer download_cache_key = "SESSION_REPLAY_DOWNLOAD_{}" session = None @@ -215,6 +226,18 @@ class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet): return False return True + def async_callback(self, *args, **kwargs): + session_id = kwargs.get('pk') + session = get_object_or_404(Session, id=session_id) + detail = i18n_fmt( + gettext_noop(f'User %s %s session %s replay'), self.request.user, + gettext_noop(ActionChoices.view), str(session) + ) + self.record_logs( + [session.asset_id], ActionChoices.download, detail, + model=Session, resource_display=str(session) + ) + def retrieve(self, request, *args, **kwargs): session_id = kwargs.get('pk') session = get_object_or_404(Session, id=session_id)