mirror of https://github.com/jumpserver/jumpserver
feat: 查看/下载录像被记录在活动日志中
parent
859268f7f3
commit
d4469aeaf7
|
@ -7,10 +7,10 @@ from rest_framework.status import HTTP_200_OK
|
||||||
from accounts import serializers
|
from accounts import serializers
|
||||||
from accounts.filters import AccountFilterSet
|
from accounts.filters import AccountFilterSet
|
||||||
from accounts.models import Account
|
from accounts.models import Account
|
||||||
|
from accounts.mixins import AccountRecordViewLogMixin
|
||||||
from assets.models import Asset, Node
|
from assets.models import Asset, Node
|
||||||
from common.api.mixin import ExtraFilterFieldsMixin
|
from common.api.mixin import ExtraFilterFieldsMixin
|
||||||
from common.permissions import UserConfirmation, ConfirmType, IsValidUser
|
from common.permissions import UserConfirmation, ConfirmType, IsValidUser
|
||||||
from common.views.mixins import RecordViewLogMixin
|
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
from rbac.permissions import RBACPermission
|
from rbac.permissions import RBACPermission
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||||
return Response(status=HTTP_200_OK)
|
return Response(status=HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
|
class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet):
|
||||||
"""
|
"""
|
||||||
因为可能要导出所有账号,所以单独建立了一个 viewset
|
因为可能要导出所有账号,所以单独建立了一个 viewset
|
||||||
"""
|
"""
|
||||||
|
@ -115,7 +115,7 @@ class AssetAccountBulkCreateApi(CreateAPIView):
|
||||||
return Response(data=serializer.data, status=HTTP_200_OK)
|
return Response(data=serializer.data, status=HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, RecordViewLogMixin, ListAPIView):
|
class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixin, ListAPIView):
|
||||||
model = Account.history.model
|
model = Account.history.model
|
||||||
serializer_class = serializers.AccountHistorySerializer
|
serializer_class = serializers.AccountHistorySerializer
|
||||||
http_method_names = ['get', 'options']
|
http_method_names = ['get', 'options']
|
||||||
|
|
|
@ -4,10 +4,10 @@ from rest_framework.response import Response
|
||||||
|
|
||||||
from accounts import serializers
|
from accounts import serializers
|
||||||
from accounts.models import AccountTemplate
|
from accounts.models import AccountTemplate
|
||||||
|
from accounts.mixins import AccountRecordViewLogMixin
|
||||||
from assets.const import Protocol
|
from assets.const import Protocol
|
||||||
from common.drf.filters import BaseFilterSet
|
from common.drf.filters import BaseFilterSet
|
||||||
from common.permissions import UserConfirmation, ConfirmType
|
from common.permissions import UserConfirmation, ConfirmType
|
||||||
from common.views.mixins import RecordViewLogMixin
|
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
from rbac.permissions import RBACPermission
|
from rbac.permissions import RBACPermission
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ class AccountTemplateViewSet(OrgBulkModelViewSet):
|
||||||
return Response(data=serializer.data)
|
return Response(data=serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class AccountTemplateSecretsViewSet(RecordViewLogMixin, AccountTemplateViewSet):
|
class AccountTemplateSecretsViewSet(AccountRecordViewLogMixin, AccountTemplateViewSet):
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'default': serializers.AccountTemplateSecretSerializer,
|
'default': serializers.AccountTemplateSecretSerializer,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -25,6 +25,7 @@ class ActionChoices(TextChoices):
|
||||||
delete = "delete", _("Delete")
|
delete = "delete", _("Delete")
|
||||||
create = "create", _("Create")
|
create = "create", _("Create")
|
||||||
# Activities action
|
# Activities action
|
||||||
|
download = "download", _("Download")
|
||||||
connect = "connect", _("Connect")
|
connect = "connect", _("Connect")
|
||||||
login = "login", _("Login")
|
login = "login", _("Login")
|
||||||
change_auth = "change_password", _("Change password")
|
change_auth = "change_password", _("Change password")
|
||||||
|
|
|
@ -88,6 +88,7 @@ class AsyncApiMixin(InterceptMixin):
|
||||||
if not self.is_need_async():
|
if not self.is_need_async():
|
||||||
return handler(*args, **kwargs)
|
return handler(*args, **kwargs)
|
||||||
resp = self.do_async(handler, *args, **kwargs)
|
resp = self.do_async(handler, *args, **kwargs)
|
||||||
|
self.async_callback(*args, **kwargs)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def is_need_refresh(self):
|
def is_need_refresh(self):
|
||||||
|
@ -98,6 +99,9 @@ class AsyncApiMixin(InterceptMixin):
|
||||||
def is_need_async(self):
|
def is_need_async(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def async_callback(self, params):
|
||||||
|
pass
|
||||||
|
|
||||||
def do_async(self, handler, *args, **kwargs):
|
def do_async(self, handler, *args, **kwargs):
|
||||||
data = self.get_cache_data()
|
data = self.get_cache_data()
|
||||||
if not data:
|
if not data:
|
||||||
|
|
|
@ -2,16 +2,14 @@
|
||||||
#
|
#
|
||||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
from django.http.response import JsonResponse
|
from django.http.response import JsonResponse
|
||||||
from django.utils import translation
|
from django.db.models import Model
|
||||||
from django.utils.translation import gettext_noop
|
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from rest_framework.request import Request
|
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.handler import create_or_update_operate_log
|
||||||
from audits.models import ActivityLog
|
from audits.models import ActivityLog
|
||||||
from common.exceptions import UserConfirmRequired
|
from common.exceptions import UserConfirmRequired
|
||||||
from common.utils import i18n_fmt
|
|
||||||
from orgs.utils import current_org
|
from orgs.utils import current_org
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -49,66 +47,19 @@ class PermissionsMixin(UserPassesTestMixin):
|
||||||
|
|
||||||
|
|
||||||
class RecordViewLogMixin:
|
class RecordViewLogMixin:
|
||||||
ACTION = ActionChoices.view
|
model: Model
|
||||||
|
|
||||||
@staticmethod
|
def record_logs(self, ids, action, detail, model=None, **kwargs):
|
||||||
def _filter_params(params):
|
model = model or self.model
|
||||||
new_params = {}
|
resource_type = model._meta.verbose_name
|
||||||
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
|
|
||||||
create_or_update_operate_log(
|
create_or_update_operate_log(
|
||||||
self.ACTION, resource_type, force=True, **kwargs
|
action, resource_type, force=True, **kwargs
|
||||||
)
|
|
||||||
detail = i18n_fmt(
|
|
||||||
gettext_noop('User %s view/export secret'), self.request.user
|
|
||||||
)
|
)
|
||||||
activities = [
|
activities = [
|
||||||
ActivityLog(
|
ActivityLog(
|
||||||
resource_id=getattr(resource_id, 'pk', resource_id),
|
resource_id=resource_id, type=ActivityChoices.operate_log,
|
||||||
type=ActivityChoices.operate_log, detail=detail, org_id=current_org.id,
|
detail=detail, org_id=current_org.id,
|
||||||
)
|
)
|
||||||
for resource_id in ids
|
for resource_id in ids
|
||||||
]
|
]
|
||||||
ActivityLog.objects.bulk_create(activities)
|
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
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.db.models import F
|
||||||
from django.http import FileResponse
|
from django.http import FileResponse
|
||||||
from django.shortcuts import get_object_or_404, reverse
|
from django.shortcuts import get_object_or_404, reverse
|
||||||
from django.utils.encoding import escape_uri_path
|
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 django_filters import rest_framework as filters
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
from rest_framework import viewsets, views
|
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.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from audits.const import ActionChoices
|
||||||
from common.api import AsyncApiMixin
|
from common.api import AsyncApiMixin
|
||||||
from common.const.http import GET
|
from common.const.http import GET
|
||||||
from common.drf.filters import BaseFilterSet
|
from common.drf.filters import BaseFilterSet
|
||||||
from common.drf.filters import DatetimeRangeFilterBackend
|
from common.drf.filters import DatetimeRangeFilterBackend
|
||||||
from common.drf.renders import PassthroughRenderer
|
from common.drf.renders import PassthroughRenderer
|
||||||
from common.storage.replay import ReplayStorageHandler
|
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.utils import get_logger, get_object_or_none
|
||||||
|
from common.views.mixins import RecordViewLogMixin
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
from orgs.utils import tmp_to_root_org, tmp_to_org
|
from orgs.utils import tmp_to_root_org, tmp_to_org
|
||||||
from rbac.permissions import RBACPermission
|
from rbac.permissions import RBACPermission
|
||||||
|
@ -70,7 +72,7 @@ class SessionFilterSet(BaseFilterSet):
|
||||||
return queryset.filter(terminal__name=value)
|
return queryset.filter(terminal__name=value)
|
||||||
|
|
||||||
|
|
||||||
class SessionViewSet(OrgBulkModelViewSet):
|
class SessionViewSet(RecordViewLogMixin, OrgBulkModelViewSet):
|
||||||
model = Session
|
model = Session
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'default': serializers.SessionSerializer,
|
'default': serializers.SessionSerializer,
|
||||||
|
@ -132,6 +134,15 @@ class SessionViewSet(OrgBulkModelViewSet):
|
||||||
filename = escape_uri_path('{}.tar'.format(storage.obj.id))
|
filename = escape_uri_path('{}.tar'.format(storage.obj.id))
|
||||||
disposition = "attachment; filename*=UTF-8''{}".format(filename)
|
disposition = "attachment; filename*=UTF-8''{}".format(filename)
|
||||||
response["Content-Disposition"] = disposition
|
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
|
return response
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -152,7 +163,7 @@ class SessionViewSet(OrgBulkModelViewSet):
|
||||||
return super().perform_create(serializer)
|
return super().perform_create(serializer)
|
||||||
|
|
||||||
|
|
||||||
class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet):
|
class SessionReplayViewSet(AsyncApiMixin, RecordViewLogMixin, viewsets.ViewSet):
|
||||||
serializer_class = serializers.ReplaySerializer
|
serializer_class = serializers.ReplaySerializer
|
||||||
download_cache_key = "SESSION_REPLAY_DOWNLOAD_{}"
|
download_cache_key = "SESSION_REPLAY_DOWNLOAD_{}"
|
||||||
session = None
|
session = None
|
||||||
|
@ -215,6 +226,18 @@ class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet):
|
||||||
return False
|
return False
|
||||||
return True
|
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):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
session_id = kwargs.get('pk')
|
session_id = kwargs.get('pk')
|
||||||
session = get_object_or_404(Session, id=session_id)
|
session = get_object_or_404(Session, id=session_id)
|
||||||
|
|
Loading…
Reference in New Issue