diff --git a/apps/accounts/api/automations/change_secret_dashboard.py b/apps/accounts/api/automations/change_secret_dashboard.py index 7214e8cdf..4d6b71266 100644 --- a/apps/accounts/api/automations/change_secret_dashboard.py +++ b/apps/accounts/api/automations/change_secret_dashboard.py @@ -90,10 +90,10 @@ class ChangeSecretDashboardApi(APIView): def get_change_secret_asset_queryset(self): qs = self.change_secrets_queryset - node_ids = qs.filter(nodes__isnull=False).values_list('nodes', flat=True).distinct() - nodes = Node.objects.filter(id__in=node_ids) + node_ids = qs.values_list('nodes', flat=True).distinct() + nodes = Node.objects.filter(id__in=node_ids).only('id', 'key') node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True) - direct_asset_ids = qs.filter(assets__isnull=False).values_list('assets', flat=True).distinct() + direct_asset_ids = qs.values_list('assets', flat=True).distinct() asset_ids = set(list(direct_asset_ids) + list(node_asset_ids)) return Asset.objects.filter(id__in=asset_ids) diff --git a/apps/assets/const/category.py b/apps/assets/const/category.py index bf109b8ff..b7ff3be12 100644 --- a/apps/assets/const/category.py +++ b/apps/assets/const/category.py @@ -20,3 +20,7 @@ class Category(ChoicesMixin, models.TextChoices): _category = getattr(cls, category.upper(), None) choices = [(_category.value, _category.label)] if _category else cls.choices return choices + + @classmethod + def as_dict(cls): + return {choice.value: choice.label for choice in cls} diff --git a/apps/assets/models/automations/base.py b/apps/assets/models/automations/base.py index cbeadea57..4bf20b5cd 100644 --- a/apps/assets/models/automations/base.py +++ b/apps/assets/models/automations/base.py @@ -53,7 +53,7 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel): return name def get_all_assets(self): - nodes = self.nodes.all() + nodes = self.nodes.only("id", "key") node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list("id", flat=True) direct_asset_ids = self.assets.all().values_list("id", flat=True) asset_ids = set(list(direct_asset_ids) + list(node_asset_ids)) diff --git a/apps/reports/api/__init__.py b/apps/reports/api/__init__.py index 67b395c54..3d2c30cb1 100644 --- a/apps/reports/api/__init__.py +++ b/apps/reports/api/__init__.py @@ -1,2 +1,3 @@ +from .assets import * from .report import * from .users import * diff --git a/apps/reports/api/assets/__init__.py b/apps/reports/api/assets/__init__.py index f48e370b5..d30f81b1f 100644 --- a/apps/reports/api/assets/__init__.py +++ b/apps/reports/api/assets/__init__.py @@ -1 +1,2 @@ +from .activity import * from .asset import * diff --git a/apps/reports/api/assets/activity.py b/apps/reports/api/assets/activity.py new file mode 100644 index 000000000..7fbfb4cb3 --- /dev/null +++ b/apps/reports/api/assets/activity.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# +from collections import defaultdict + +from django.db.models import Count, Q +from django.http.response import JsonResponse +from rest_framework.views import APIView + +from assets.const import AllTypes +from assets.models import Asset +from common.permissions import IsValidLicense +from common.utils import lazyproperty +from rbac.permissions import RBACPermission +from reports.api.assets.base import group_stats +from reports.mixins import DateRangeMixin +from terminal.const import LoginFrom +from terminal.models import Session + +__all__ = ['AssetActivityApi'] + + +class AssetActivityApi(DateRangeMixin, APIView): + http_method_names = ['get'] + # TODO: Define the required RBAC permissions for this API + rbac_perms = { + 'GET': 'terminal.view_session', + } + permission_classes = [RBACPermission, IsValidLicense] + + def get_asset_login_metrics(self, queryset): + data = defaultdict(set) + for t, asset in queryset.values_list('date_start', 'asset'): + date_str = str(t.date()) + data[date_str].add(asset) + + metrics = [len(data.get(str(d), set())) for d in self.date_range_list] + return metrics + + @lazyproperty + def session_qs(self): + return Session.objects.all() + + def get(self, request, *args, **kwargs): + qs = self.session_qs + qs = self.filter_by_date_range(qs, 'date_start') + all_type_dict = dict(AllTypes.choices()) + + stats = qs.aggregate( + total=Count(1), + asset_online=Count(1, filter=Q(is_finished=False)), + asset_count=Count('asset_id', distinct=True), + user_count=Count('user_id', distinct=True), + is_success_count=Count(1, filter=Q(is_success=True)), + ) + + asset_ids = {str(_id) for _id in qs.values_list('asset_id', flat=True).distinct()} + assets = Asset.objects.filter(id__in=asset_ids) + + asset_login_by_protocol = group_stats( + qs, 'protocol_label', 'protocol' + ) + + asset_login_by_from = group_stats( + qs, 'login_from_label', 'login_from', LoginFrom.as_dict() + ) + + asset_by_type = group_stats( + assets, 'type', 'platform__type', all_type_dict, + ) + dates_metrics_date = [date.strftime('%m-%d') for date in self.date_range_list] or ['0'] + + payload = { + **stats, + 'asset_login_by_type': asset_by_type, + 'asset_login_by_from': asset_login_by_from, + 'asset_login_by_protocol': asset_login_by_protocol, + 'asset_login_log_metrics': { + 'dates_metrics_date': dates_metrics_date, + 'dates_metrics_total': self.get_asset_login_metrics(qs), + } + } + return JsonResponse(payload, status=200) diff --git a/apps/reports/api/assets/asset.py b/apps/reports/api/assets/asset.py index f261ff5c0..212f8b6e0 100644 --- a/apps/reports/api/assets/asset.py +++ b/apps/reports/api/assets/asset.py @@ -1,16 +1,17 @@ # -*- coding: utf-8 -*- # -from django.db.models import Count, Q, F +from django.db.models import Count, Q from django.http import JsonResponse from django.utils import timezone from rest_framework.views import APIView from assets.const import AllTypes, Connectivity, Category -from assets.models import Asset +from assets.models import Asset, Platform from common.permissions import IsValidLicense from common.utils import lazyproperty from rbac.permissions import RBACPermission +from reports.api.assets.base import group_stats __all__ = ['AssetStatisticApi'] @@ -26,56 +27,48 @@ class AssetStatisticApi(APIView): @lazyproperty def base_qs(self): return Asset.objects.only( - 'id', 'platform', 'zone', 'connectivity', 'created_time' + 'id', 'platform', 'zone', 'connectivity', 'is_active' ) - @staticmethod - def _group_stats(queryset, alias, key, label_map=None): - grouped = ( - queryset - .values(**{alias: F(key)}) - .annotate(total=Count('id')) - ) - - data = [ - { - alias: val, - 'total': cnt, - **({'label': label_map.get(val, val)} if label_map else {}) - } - for val, cnt in grouped.values_list(alias, 'total') - ] - - return data - def get(self, request, *args, **kwargs): qs = self.base_qs + all_type_dict = dict(AllTypes.choices()) + + platform_by_type = group_stats( + Platform.objects.all(), 'type_label', 'type', all_type_dict, + ) stats = qs.aggregate( - total=Count('id'), - active=Count('id', filter=Q(is_active=True)), - connected=Count('id', filter=Q(connectivity=Connectivity.OK)), + total=Count(1), + active=Count(1, filter=Q(is_active=True)), + connected=Count(1, filter=Q(connectivity=Connectivity.OK)), ) - by_type = self._group_stats( - qs, 'type', 'platform__type', dict(AllTypes.choices()), + by_type = group_stats( + qs, 'type', 'platform__type', all_type_dict, ) - by_category = self._group_stats( - qs, 'category', 'platform__category', dict(Category.choices()) + by_category = group_stats( + qs, 'category', 'platform__category', Category.as_dict() ) - by_zone = self._group_stats( - qs, 'zone', 'zone__name' + by_zone = group_stats( + qs, 'zone_label', 'zone__name' ) week_start = timezone.now() + timezone.timedelta(days=7) - assets_added_this_week = qs.filter(date_created__gte=week_start).count() + assets_added_this_week_qs = qs.filter(date_created__gte=week_start) + assets_added_this_week_by_type = group_stats( + assets_added_this_week_qs, 'type', 'platform__type', all_type_dict, + ) + payload = { **stats, - 'assets_by_platform_type': by_type, + 'platform_by_type': platform_by_type, + 'assets_by_type': by_type, 'assets_by_category': by_category, 'assets_by_zone': by_zone, - 'assets_added_this_week': assets_added_this_week, + 'assets_added_this_week_count': assets_added_this_week_qs.count(), + 'assets_added_this_week_by_type': assets_added_this_week_by_type, } return JsonResponse(payload, status=200) diff --git a/apps/reports/api/assets/base.py b/apps/reports/api/assets/base.py new file mode 100644 index 000000000..f905777d4 --- /dev/null +++ b/apps/reports/api/assets/base.py @@ -0,0 +1,21 @@ +from django.db.models import Count, F + + +def group_stats(queryset, alias, key, label_map=None): + grouped = ( + queryset + .exclude(**{f'{key}__isnull': True}) + .values(**{alias: F(key)}) + .annotate(total=Count('id')) + ) + + data = [ + { + alias: val, + 'total': cnt, + **({'label': label_map.get(val, val)} if label_map else {}) + } + for val, cnt in grouped.values_list(alias, 'total') + ] + + return data diff --git a/apps/reports/urls/api_urls.py b/apps/reports/urls/api_urls.py index 69a951691..e6296855d 100644 --- a/apps/reports/urls/api_urls.py +++ b/apps/reports/urls/api_urls.py @@ -7,5 +7,7 @@ app_name = 'reports' urlpatterns = [ path('reports/', api.ReportViewSet.as_view(), name='report-list'), path('reports/users/', api.UserReportApi.as_view(), name='user-list'), - path('reports/user-change-password/', api.UserChangeSecretApi.as_view(), name='user-change-password') + path('reports/user-change-password/', api.UserChangeSecretApi.as_view(), name='user-change-password'), + path('reports/asset-statistic/', api.AssetStatisticApi.as_view(), name='asset-statistic'), + path('reports/asset-activity/', api.AssetActivityApi.as_view(), name='asset-activity') ] diff --git a/apps/terminal/const.py b/apps/terminal/const.py index 36ea0ee80..099cdcab8 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -106,3 +106,14 @@ class SessionErrorReason(TextChoices): replay_upload_failed = 'replay_upload_failed', _('Replay upload failed') replay_convert_failed = 'replay_convert_failed', _('Replay convert failed') replay_unsupported = 'replay_unsupported', _('Replay unsupported') + + +class LoginFrom(TextChoices): + ST = 'ST', 'SSH Terminal' + RT = 'RT', 'RDP Terminal' + WT = 'WT', 'Web Terminal' + DT = 'DT', 'DB Terminal' + + @classmethod + def as_dict(cls): + return {choice.value: choice.label for choice in cls} diff --git a/apps/terminal/models/session/session.py b/apps/terminal/models/session/session.py index 5681836e6..b9ac46872 100644 --- a/apps/terminal/models/session/session.py +++ b/apps/terminal/models/session/session.py @@ -17,17 +17,11 @@ from common.const.signals import OP_LOG_SKIP_SIGNAL from common.utils import get_object_or_none, lazyproperty from orgs.mixins.models import OrgModelMixin from terminal.backends import get_multi_command_storage -from terminal.const import SessionType, TerminalType +from terminal.const import SessionType, TerminalType, LoginFrom from users.models import User class Session(OrgModelMixin): - class LOGIN_FROM(models.TextChoices): - ST = 'ST', 'SSH Terminal' - RT = 'RT', 'RDP Terminal' - WT = 'WT', 'Web Terminal' - DT = 'DT', 'DB Terminal' - id = models.UUIDField(default=uuid.uuid4, primary_key=True) user = models.CharField(max_length=128, verbose_name=_("User"), db_index=True) user_id = models.CharField(blank=True, default='', max_length=36, db_index=True) @@ -36,7 +30,7 @@ class Session(OrgModelMixin): account = models.CharField(max_length=128, verbose_name=_("Account"), db_index=True) account_id = models.CharField(max_length=128, verbose_name=_("Account ID"), db_index=True) protocol = models.CharField(default='ssh', max_length=16, db_index=True) - login_from = models.CharField(max_length=2, choices=LOGIN_FROM.choices, default="ST", verbose_name=_("Login from")) + login_from = models.CharField(max_length=2, choices=LoginFrom.choices, default="ST", verbose_name=_("Login from")) type = models.CharField(max_length=16, default='normal', db_index=True) remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) is_success = models.BooleanField(default=True, db_index=True) diff --git a/apps/terminal/models/session/sharing.py b/apps/terminal/models/session/sharing.py index 11e06953d..7e99d6688 100644 --- a/apps/terminal/models/session/sharing.py +++ b/apps/terminal/models/session/sharing.py @@ -2,15 +2,15 @@ import datetime from django.db import models from django.utils import timezone -from django.utils.translation import gettext_lazy as _ from django.utils.functional import cached_property +from django.utils.translation import gettext_lazy as _ from common.db.models import JMSBaseModel from common.utils import is_uuid from orgs.mixins.models import OrgModelMixin from orgs.utils import tmp_to_root_org +from terminal.const import LoginFrom from users.models import User -from .session import Session __all__ = ['SessionSharing', 'SessionJoinRecord'] @@ -89,8 +89,6 @@ class SessionSharing(JMSBaseModel, OrgModelMixin): class SessionJoinRecord(JMSBaseModel, OrgModelMixin): - LOGIN_FROM = Session.LOGIN_FROM - session = models.ForeignKey( 'terminal.Session', on_delete=models.CASCADE, verbose_name=_('Session') ) @@ -114,7 +112,7 @@ class SessionJoinRecord(JMSBaseModel, OrgModelMixin): db_index=True ) login_from = models.CharField( - max_length=2, choices=LOGIN_FROM.choices, default="WT", + max_length=2, choices=LoginFrom.choices, default="WT", verbose_name=_("Login from") ) is_success = models.BooleanField(