diff --git a/apps/common/filters.py b/apps/common/filters.py new file mode 100644 index 000000000..701f9c730 --- /dev/null +++ b/apps/common/filters.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import filters +from rest_framework.fields import DateTimeField +from rest_framework.serializers import ValidationError +import logging + +__all__ = ["DatetimeRangeFilter"] + + +class DatetimeRangeFilter(filters.BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + if not hasattr(view, 'date_range_filter_fields'): + return queryset + try: + fields = dict(view.date_range_filter_fields) + except ValueError: + msg = "View {} datetime_filter_fields set is error".format(view.name) + logging.error(msg) + return queryset + kwargs = {} + for attr, date_range_keyword in fields.items(): + if len(date_range_keyword) != 2: + continue + for i, v in enumerate(date_range_keyword): + value = request.query_params.get(v) + if not value: + continue + try: + field = DateTimeField() + value = field.to_internal_value(value) + if i == 0: + lookup = "__gte" + else: + lookup = "__lte" + kwargs[attr+lookup] = value + except ValidationError as e: + print(e) + continue + if kwargs: + queryset = queryset.filter(**kwargs) + return queryset diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py index 410f077eb..2bd1b5a16 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -38,8 +38,10 @@ class IDInFilterMixin(object): class IDInCacheFilterMixin(object): def filter_queryset(self, queryset): - queryset = super(IDInCacheFilterMixin, self).filter_queryset(queryset) + queryset = super().filter_queryset(queryset) spm = self.request.query_params.get('spm') + if not spm: + return queryset cache_key = KEY_CACHE_RESOURCES_ID.format(spm) resources_id = cache.get(cache_key) if resources_id and isinstance(resources_id, list): diff --git a/apps/common/mixins/serializers.py b/apps/common/mixins/serializers.py index 009d0b994..008cd5eee 100644 --- a/apps/common/mixins/serializers.py +++ b/apps/common/mixins/serializers.py @@ -27,13 +27,14 @@ class BulkSerializerMixin(object): if all((isinstance(self.root, BulkListSerializer), id_attr, request_method in ('PUT', 'PATCH'))): - id_field = self.fields[id_attr] + id_field = self.fields.get("id") or self.fields.get('pk') if data.get("id"): id_value = id_field.to_internal_value(data.get("id")) else: id_value = id_field.to_internal_value(data.get("pk")) + print(">>>>>>>>>>>>>>>>>>>") + print(id_attr) ret[id_attr] = id_value - return ret diff --git a/apps/orgs/mixins.py b/apps/orgs/mixins.py deleted file mode 100644 index 228b5b102..000000000 --- a/apps/orgs/mixins.py +++ /dev/null @@ -1,216 +0,0 @@ -# -*- coding: utf-8 -*- -# -import traceback -from django.db import models -from django.utils.translation import ugettext_lazy as _ -from django.shortcuts import redirect, get_object_or_404 -from django import forms -from django.core.exceptions import ValidationError -from django.http.response import HttpResponseForbidden -from rest_framework import serializers -from rest_framework.validators import UniqueTogetherValidator - -from common.utils import get_logger -from common.validators import ProjectUniqueValidator -from common.mixins import BulkSerializerMixin -from .utils import ( - set_current_org, set_to_root_org, get_current_org, current_org, - get_current_org_id_for_serializer, -) -from .models import Organization - -logger = get_logger(__file__) - -__all__ = [ - 'OrgManager', 'OrgViewGenericMixin', 'OrgModelMixin', 'OrgModelForm', - 'RootOrgViewMixin', 'OrgMembershipSerializerMixin', - 'OrgMembershipModelViewSetMixin', 'OrgResourceSerializerMixin', - 'BulkOrgResourceSerializerMixin', 'BulkOrgResourceModelSerializer', -] - - -class OrgManager(models.Manager): - - def get_queryset(self): - queryset = super(OrgManager, self).get_queryset() - kwargs = {} - - _current_org = get_current_org() - if _current_org is None: - kwargs['id'] = None - elif _current_org.is_real(): - kwargs['org_id'] = _current_org.id - elif _current_org.is_default(): - queryset = queryset.filter(org_id="") - - # lines = traceback.format_stack() - # print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>") - # for line in lines[-10:-5]: - # print(line) - # print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<") - - queryset = queryset.filter(**kwargs) - return queryset - - def all(self): - if not current_org: - msg = 'You can `objects.set_current_org(org).all()` then run it' - return self - else: - return super(OrgManager, self).all() - - def set_current_org(self, org): - if isinstance(org, str): - org = Organization.get_instance(org) - set_current_org(org) - return self - - -class OrgModelMixin(models.Model): - org_id = models.CharField(max_length=36, blank=True, default='', - verbose_name=_("Organization"), db_index=True) - objects = OrgManager() - - sep = '@' - - def save(self, *args, **kwargs): - if current_org is not None and current_org.is_real(): - self.org_id = current_org.id - return super().save(*args, **kwargs) - - @property - def org(self): - from orgs.models import Organization - org = Organization.get_instance(self.org_id) - return org - - @property - def org_name(self): - return self.org.name - - @property - def fullname(self, attr=None): - name = '' - if attr and hasattr(self, attr): - name = getattr(self, attr) - elif hasattr(self, 'name'): - name = self.name - elif hasattr(self, 'hostname'): - name = self.hostname - if self.org.is_real(): - return name + self.sep + self.org_name - else: - return name - - def validate_unique(self, exclude=None): - """ - Check unique constraints on the model and raise ValidationError if any - failed. - Form 提交时会使用这个检验 - """ - self.org_id = current_org.id if current_org.is_real() else '' - if exclude and 'org_id' in exclude: - exclude.remove('org_id') - unique_checks, date_checks = self._get_unique_checks(exclude=exclude) - - errors = self._perform_unique_checks(unique_checks) - date_errors = self._perform_date_checks(date_checks) - - for k, v in date_errors.items(): - errors.setdefault(k, []).extend(v) - - if errors: - raise ValidationError(errors) - - class Meta: - abstract = True - - -class OrgViewGenericMixin: - def dispatch(self, request, *args, **kwargs): - if current_org is None: - return redirect('orgs:switch-a-org') - - if not current_org.can_admin_by(request.user): - if request.user.is_org_admin: - return redirect('orgs:switch-a-org') - return HttpResponseForbidden() - return super().dispatch(request, *args, **kwargs) - - -class RootOrgViewMixin: - def dispatch(self, request, *args, **kwargs): - set_to_root_org() - return super().dispatch(request, *args, **kwargs) - - -class OrgModelForm(forms.ModelForm): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - for name, field in self.fields.items(): - if not hasattr(field, 'queryset'): - continue - model = field.queryset.model - field.queryset = model.objects.all() - - -class OrgMembershipSerializerMixin: - def run_validation(self, initial_data=None): - initial_data['organization'] = str(self.context['org'].id) - return super().run_validation(initial_data) - - -class OrgMembershipModelViewSetMixin: - org = None - membership_class = None - lookup_field = 'user' - lookup_url_kwarg = 'user_id' - http_method_names = ['get', 'post', 'delete', 'head', 'options'] - - def dispatch(self, request, *args, **kwargs): - self.org = get_object_or_404(Organization, pk=kwargs.get('org_id')) - return super().dispatch(request, *args, **kwargs) - - def get_serializer_context(self): - context = super().get_serializer_context() - context['org'] = self.org - return context - - def get_queryset(self): - queryset = self.membership_class.objects.filter(organization=self.org) - return queryset - - -class OrgResourceSerializerMixin(serializers.Serializer): - """ - 通过API批量操作资源时, 自动给每个资源添加所需属性org_id的值为current_org_id - (同时为serializer.is_valid()对Model的unique_together校验做准备) - 由于HiddenField字段不可读,API获取资产信息时获取不到org_id, - 但是coco需要资产的org_id字段,所以修改为CharField类型 - """ - org_id = serializers.ReadOnlyField(default=get_current_org_id_for_serializer, label=_("Organization")) - org_name = serializers.ReadOnlyField(label=_("Org name")) - - def get_validators(self): - _validators = super().get_validators() - validators = [] - - for v in _validators: - if isinstance(v, UniqueTogetherValidator) \ - and "org_id" in v.fields: - v = ProjectUniqueValidator(v.queryset, v.fields) - validators.append(v) - return validators - - def get_field_names(self, declared_fields, info): - fields = super().get_field_names(declared_fields, info) - fields.extend(["org_id", "org_name"]) - return fields - - -class BulkOrgResourceSerializerMixin(OrgResourceSerializerMixin, BulkSerializerMixin): - pass - - -class BulkOrgResourceModelSerializer(BulkOrgResourceSerializerMixin, serializers.ModelSerializer): - pass diff --git a/apps/orgs/mixins/__init__.py b/apps/orgs/mixins/__init__.py new file mode 100644 index 000000000..3d1a4a7db --- /dev/null +++ b/apps/orgs/mixins/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# + +from .models import * +from .serializers import * +from .forms import * +from .api import * diff --git a/apps/orgs/mixins/api.py b/apps/orgs/mixins/api.py new file mode 100644 index 000000000..5e0cef5c3 --- /dev/null +++ b/apps/orgs/mixins/api.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# +from django.shortcuts import get_object_or_404 +from rest_framework.viewsets import ModelViewSet +from rest_framework_bulk import BulkModelViewSet +from common.mixins import IDInCacheFilterMixin + +from ..utils import set_to_root_org +from ..models import Organization + +__all__ = [ + 'RootOrgViewMixin', 'OrgMembershipModelViewSetMixin', 'OrgModelViewSet', + 'OrgBulkModelViewSet', +] + + +class RootOrgViewMixin: + def dispatch(self, request, *args, **kwargs): + set_to_root_org() + return super().dispatch(request, *args, **kwargs) + + +class OrgModelViewSet(IDInCacheFilterMixin, ModelViewSet): + def get_queryset(self): + return super().get_queryset().all() + + +class OrgBulkModelViewSet(IDInCacheFilterMixin, BulkModelViewSet): + def get_queryset(self): + return super().get_queryset().all() + + +class OrgMembershipModelViewSetMixin: + org = None + membership_class = None + lookup_field = 'user' + lookup_url_kwarg = 'user_id' + http_method_names = ['get', 'post', 'delete', 'head', 'options'] + + def dispatch(self, request, *args, **kwargs): + self.org = get_object_or_404(Organization, pk=kwargs.get('org_id')) + return super().dispatch(request, *args, **kwargs) + + def get_serializer_context(self): + context = super().get_serializer_context() + context['org'] = self.org + return context + + def get_queryset(self): + queryset = self.membership_class.objects.filter(organization=self.org) + return queryset diff --git a/apps/orgs/mixins/forms.py b/apps/orgs/mixins/forms.py new file mode 100644 index 000000000..49ec106f1 --- /dev/null +++ b/apps/orgs/mixins/forms.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# + +from django import forms + +__all__ = ['OrgModelForm'] + + +class OrgModelForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for name, field in self.fields.items(): + if not hasattr(field, 'queryset'): + continue + model = field.queryset.model + field.queryset = model.objects.all() diff --git a/apps/orgs/mixins/models.py b/apps/orgs/mixins/models.py new file mode 100644 index 000000000..d579bab37 --- /dev/null +++ b/apps/orgs/mixins/models.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# + +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.core.exceptions import ValidationError + +from common.utils import get_logger +from ..utils import ( + set_current_org, get_current_org, current_org, +) +from ..models import Organization + +logger = get_logger(__file__) + +__all__ = [ + 'OrgManager', 'OrgModelMixin', +] + + +class OrgManager(models.Manager): + + def get_queryset(self): + queryset = super(OrgManager, self).get_queryset() + kwargs = {} + + _current_org = get_current_org() + if _current_org is None: + kwargs['id'] = None + elif _current_org.is_real(): + kwargs['org_id'] = _current_org.id + elif _current_org.is_default(): + queryset = queryset.filter(org_id="") + + # lines = traceback.format_stack() + # print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>") + # for line in lines[-10:-5]: + # print(line) + # print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<") + + queryset = queryset.filter(**kwargs) + return queryset + + def all(self): + if not current_org: + msg = 'You can `objects.set_current_org(org).all()` then run it' + return self + else: + return super(OrgManager, self).all() + + def set_current_org(self, org): + if isinstance(org, str): + org = Organization.get_instance(org) + set_current_org(org) + return self + + +class OrgModelMixin(models.Model): + org_id = models.CharField(max_length=36, blank=True, default='', + verbose_name=_("Organization"), db_index=True) + objects = OrgManager() + + sep = '@' + + def save(self, *args, **kwargs): + if current_org is not None and current_org.is_real(): + self.org_id = current_org.id + return super().save(*args, **kwargs) + + @property + def org(self): + from orgs.models import Organization + org = Organization.get_instance(self.org_id) + return org + + @property + def org_name(self): + return self.org.name + + @property + def fullname(self, attr=None): + name = '' + if attr and hasattr(self, attr): + name = getattr(self, attr) + elif hasattr(self, 'name'): + name = self.name + elif hasattr(self, 'hostname'): + name = self.hostname + if self.org.is_real(): + return name + self.sep + self.org_name + else: + return name + + def validate_unique(self, exclude=None): + """ + Check unique constraints on the model and raise ValidationError if any + failed. + Form 提交时会使用这个检验 + """ + self.org_id = current_org.id if current_org.is_real() else '' + if exclude and 'org_id' in exclude: + exclude.remove('org_id') + unique_checks, date_checks = self._get_unique_checks(exclude=exclude) + + errors = self._perform_unique_checks(unique_checks) + date_errors = self._perform_date_checks(date_checks) + + for k, v in date_errors.items(): + errors.setdefault(k, []).extend(v) + + if errors: + raise ValidationError(errors) + + class Meta: + abstract = True diff --git a/apps/orgs/mixins/serializers.py b/apps/orgs/mixins/serializers.py new file mode 100644 index 000000000..ea4e4bd6b --- /dev/null +++ b/apps/orgs/mixins/serializers.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers +from rest_framework.validators import UniqueTogetherValidator + +from common.validators import ProjectUniqueValidator +from common.mixins import BulkSerializerMixin +from ..utils import get_current_org_id_for_serializer + + +__all__ = [ + "OrgResourceSerializerMixin", "BulkOrgResourceSerializerMixin", + "BulkOrgResourceModelSerializer", "OrgMembershipSerializerMixin" +] + + +class OrgResourceSerializerMixin(serializers.Serializer): + """ + 通过API批量操作资源时, 自动给每个资源添加所需属性org_id的值为current_org_id + (同时为serializer.is_valid()对Model的unique_together校验做准备) + 由于HiddenField字段不可读,API获取资产信息时获取不到org_id, + 但是coco需要资产的org_id字段,所以修改为CharField类型 + """ + org_id = serializers.ReadOnlyField(default=get_current_org_id_for_serializer, label=_("Organization")) + org_name = serializers.ReadOnlyField(label=_("Org name")) + + def get_validators(self): + _validators = super().get_validators() + validators = [] + + for v in _validators: + if isinstance(v, UniqueTogetherValidator) \ + and "org_id" in v.fields: + v = ProjectUniqueValidator(v.queryset, v.fields) + validators.append(v) + return validators + + def get_field_names(self, declared_fields, info): + fields = super().get_field_names(declared_fields, info) + fields.extend(["org_id", "org_name"]) + return fields + + +class BulkOrgResourceSerializerMixin(OrgResourceSerializerMixin, BulkSerializerMixin): + pass + + +class BulkOrgResourceModelSerializer(BulkOrgResourceSerializerMixin, serializers.ModelSerializer): + pass + + +class OrgMembershipSerializerMixin: + def run_validation(self, initial_data=None): + initial_data['organization'] = str(self.context['org'].id) + return super().run_validation(initial_data) diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index 1b8b9321e..b22516863 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -1108,9 +1108,44 @@ function formatDateAsCN(d) { function getUrlParams(url) { url = url.split("?"); - let params = ""; + var params = ""; if (url.length === 2){ params = url[1]; } return params +} + +function getTimeUnits(u) { + var units = { + "d": "天", + "h": "时", + "m": "分", + "s": "秒", + }; + if (navigator.language || "zh-CN") { + return units[u] + } + return u +} + +function timeOffset(a, b) { + var start = new Date(a); + var end = new Date(b); + var offset = (end - start)/1000; + + var days = offset / 3600 / 24; + var hours = offset / 3600; + var minutes = offset / 60; + var seconds = offset; + + if (days > 1) { + return days.toFixed(1) + " " + getTimeUnits("d"); + } else if (hours > 1) { + return hours.toFixed(1) + " " + getTimeUnits("h"); + } else if (minutes > 1) { + return minutes.toFixed(1) + " " + getTimeUnits("m") + } else if (seconds > 1) { + return seconds.toFixed(1) + " " + getTimeUnits("s") + } + return "" } \ No newline at end of file diff --git a/apps/terminal/api/session.py b/apps/terminal/api/session.py index deb005b77..e81772fae 100644 --- a/apps/terminal/api/session.py +++ b/apps/terminal/api/session.py @@ -9,12 +9,13 @@ from django.conf import settings from rest_framework.pagination import LimitOffsetPagination from rest_framework import viewsets from rest_framework.response import Response -from rest_framework_bulk import BulkModelViewSet +from rest_framework.generics import GenericAPIView import jms_storage - from common.utils import is_uuid, get_logger from common.permissions import IsOrgAdminOrAppUser, IsAuditor +from common.filters import DatetimeRangeFilter +from orgs.mixins import OrgBulkModelViewSet from ..hands import SystemUser from ..models import Session from .. import serializers @@ -24,12 +25,17 @@ __all__ = ['SessionViewSet', 'SessionReplayViewSet',] logger = get_logger(__name__) -class SessionViewSet(BulkModelViewSet): +class SessionViewSet(OrgBulkModelViewSet): queryset = Session.objects.all() serializer_class = serializers.SessionSerializer pagination_class = LimitOffsetPagination permission_classes = (IsOrgAdminOrAppUser | IsAuditor, ) - filter_fields = ["user", "asset", "system_user", "terminal"] + filter_fields = [ + "user", "asset", "system_user", "terminal", "is_finished", + ] + date_range_filter_fields = [ + ('date_start', ('date_from', 'date_to')) + ] def get_object(self): # 解决guacamole更新session时并发导致幽灵会话的问题 @@ -38,6 +44,12 @@ class SessionViewSet(BulkModelViewSet): obj = obj.select_for_update() return obj + @property + def filter_backends(self): + backends = list(GenericAPIView.filter_backends) + backends.append(DatetimeRangeFilter) + return backends + def perform_create(self, serializer): if hasattr(self.request.user, 'terminal'): serializer.validated_data["terminal"] = self.request.user.terminal diff --git a/apps/terminal/models.py b/apps/terminal/models.py index 0bcceef58..2c5c7d666 100644 --- a/apps/terminal/models.py +++ b/apps/terminal/models.py @@ -241,6 +241,10 @@ class Session(OrgModelMixin): command_store = get_multi_command_storage() return command_store.count(session=str(self.id)) + @property + def login_from_display(self): + return self.get_login_from_display() + class Meta: db_table = "terminal_session" ordering = ["-date_start"] diff --git a/apps/terminal/serializers/v1.py b/apps/terminal/serializers/v1.py index 27b12b77a..10898ebd6 100644 --- a/apps/terminal/serializers/v1.py +++ b/apps/terminal/serializers/v1.py @@ -2,6 +2,7 @@ # from rest_framework import serializers +from orgs.mixins import BulkOrgResourceModelSerializer from common.mixins import BulkSerializerMixin from common.serializers import AdaptedBulkListSerializer from ..models import Terminal, Status, Session, Task @@ -24,13 +25,18 @@ class TerminalSerializer(serializers.ModelSerializer): return Session.objects.filter(terminal=obj, is_finished=False).count() -class SessionSerializer(BulkSerializerMixin, serializers.ModelSerializer): +class SessionSerializer(BulkOrgResourceModelSerializer): command_amount = serializers.IntegerField(read_only=True) class Meta: model = Session list_serializer_class = AdaptedBulkListSerializer - fields = '__all__' + fields = [ + "id", "user", "asset", "system_user", "login_from", + "login_from_display", "remote_addr", "is_finished", + "has_replay", "can_replay", "protocol", "date_start", "date_end", + "terminal", "command_amount", + ] class StatusSerializer(serializers.ModelSerializer): diff --git a/apps/terminal/templates/terminal/command_list.html b/apps/terminal/templates/terminal/command_list.html index addece387..751e63ac9 100644 --- a/apps/terminal/templates/terminal/command_list.html +++ b/apps/terminal/templates/terminal/command_list.html @@ -8,7 +8,6 @@ .toggle { cursor: pointer; } - .detail-key { width: 70px; } diff --git a/apps/terminal/templates/terminal/session_list.html b/apps/terminal/templates/terminal/session_list.html index 44bbaa32b..db70bce69 100644 --- a/apps/terminal/templates/terminal/session_list.html +++ b/apps/terminal/templates/terminal/session_list.html @@ -7,116 +7,41 @@ - {% endblock %} {% block content_left_head %} {% endblock %} +{% block table_pagination %} +{% endblock %} {% block table_search %} -
{% endblock %} -{% block table_head %} -+ | {% trans 'ID' %} | +{% trans 'User' %} | +{% trans 'Asset' %} | +{% trans 'System user' %} | +{% trans 'Remote addr' %} | +{% trans 'Protocol' %} | +{% trans 'Login from' %} | +{% trans 'Command' %} | +{% trans 'Date start' %} | +{% trans 'Duration' %} | +{% trans 'Action' %} |
---|