mirror of https://github.com/jumpserver/jumpserver
perf: user change password
parent
7f46f32200
commit
dab97ea16c
|
@ -167,7 +167,7 @@ class UserLoginLogViewSet(UserLoginCommonMixin, OrgReadonlyModelViewSet):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
queryset = queryset.model.filter_login_queryset_by_org(queryset)
|
queryset = queryset.model.filter_queryset_by_org(queryset)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
@ -289,12 +289,7 @@ class PasswordChangeLogViewSet(OrgReadonlyModelViewSet):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
if not current_org.is_root():
|
return self.model.filter_queryset_by_org(queryset)
|
||||||
users = current_org.get_members()
|
|
||||||
queryset = queryset.filter(
|
|
||||||
user__in=[str(user) for user in users]
|
|
||||||
)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
||||||
|
|
|
@ -186,6 +186,15 @@ class PasswordChangeLog(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Password change log")
|
verbose_name = _("Password change log")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def filter_queryset_by_org(queryset):
|
||||||
|
if not current_org.is_root():
|
||||||
|
users = current_org.get_members()
|
||||||
|
queryset = queryset.filter(
|
||||||
|
user__in=[str(user) for user in users]
|
||||||
|
)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class UserLoginLog(models.Model):
|
class UserLoginLog(models.Model):
|
||||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
|
@ -256,7 +265,7 @@ class UserLoginLog(models.Model):
|
||||||
return reason
|
return reason
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def filter_login_queryset_by_org(queryset):
|
def filter_queryset_by_org(queryset):
|
||||||
from audits.utils import construct_userlogin_usernames
|
from audits.utils import construct_userlogin_usernames
|
||||||
if current_org.is_root() or not settings.XPACK_ENABLED:
|
if current_org.is_root() or not settings.XPACK_ENABLED:
|
||||||
return queryset
|
return queryset
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
|
from .change_password import *
|
||||||
from .user import *
|
from .user import *
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.http.response import JsonResponse
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from audits.models import PasswordChangeLog
|
||||||
|
from common.permissions import IsValidLicense
|
||||||
|
from common.utils import lazyproperty, get_ip_city, get_logger
|
||||||
|
from rbac.permissions import RBACPermission
|
||||||
|
from reports.mixins import DateRangeMixin
|
||||||
|
|
||||||
|
__all__ = ['UserChangeSecretApi']
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
class UserChangeSecretApi(DateRangeMixin, APIView):
|
||||||
|
http_method_names = ['get']
|
||||||
|
# TODO: Define the required RBAC permissions for this API
|
||||||
|
rbac_perms = {
|
||||||
|
'GET': 'users..view_users',
|
||||||
|
}
|
||||||
|
permission_classes = [RBACPermission, IsValidLicense]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_change_password_region_distribution(queryset):
|
||||||
|
unique_ips = queryset.values_list('remote_addr', flat=True).distinct()
|
||||||
|
data = defaultdict(int)
|
||||||
|
for ip in unique_ips:
|
||||||
|
try:
|
||||||
|
city = str(get_ip_city(ip))
|
||||||
|
if not city:
|
||||||
|
continue
|
||||||
|
data[city] += 1
|
||||||
|
except Exception:
|
||||||
|
logger.debug(f"Failed to get city for IP {ip}, skipping", exc_info=True)
|
||||||
|
continue
|
||||||
|
return dict(data)
|
||||||
|
|
||||||
|
def get_change_password_metrics(self, queryset):
|
||||||
|
filtered_queryset = self.filter_by_date_range(queryset, 'datetime')
|
||||||
|
|
||||||
|
data = defaultdict(set)
|
||||||
|
for t, username in filtered_queryset.values_list('datetime', 'user'):
|
||||||
|
date_str = str(t.date())
|
||||||
|
data[date_str].add(username)
|
||||||
|
|
||||||
|
metrics = [len(data.get(str(d), set())) for d in self.date_range_list]
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def change_password_queryset(self):
|
||||||
|
queryset = PasswordChangeLog.objects.all()
|
||||||
|
return PasswordChangeLog.filter_queryset_by_org(queryset)
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
data = {}
|
||||||
|
dates_metrics_date = [date.strftime('%m-%d') for date in self.date_range_list] or ['0']
|
||||||
|
|
||||||
|
qs = self.filter_by_date_range(self.change_password_queryset, 'datetime')
|
||||||
|
|
||||||
|
total = qs.count()
|
||||||
|
change_password_top10_users = qs.values(
|
||||||
|
'user').annotate(user_count=Count('id')).order_by('-user_count')[:10]
|
||||||
|
|
||||||
|
change_password_top10_change_bys = qs.values(
|
||||||
|
'change_by').annotate(user_count=Count('id')).order_by('-user_count')[:10]
|
||||||
|
|
||||||
|
data['total'] = total
|
||||||
|
data['user_total'] = qs.values('user').distinct().count()
|
||||||
|
data['change_by_total'] = qs.values('change_by').distinct().count()
|
||||||
|
data['change_password_top10_users'] = list(change_password_top10_users)
|
||||||
|
data['change_password_top10_change_bys'] = list(change_password_top10_change_bys)
|
||||||
|
|
||||||
|
data['user_change_password_metrics'] = {
|
||||||
|
'dates_metrics_date': dates_metrics_date,
|
||||||
|
'dates_metrics_total': self.get_change_password_metrics(qs),
|
||||||
|
}
|
||||||
|
|
||||||
|
data['change_password_region_distribution'] = self.get_change_password_region_distribution(qs)
|
||||||
|
return JsonResponse(data, status=200)
|
|
@ -4,20 +4,19 @@ from collections import defaultdict
|
||||||
|
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.http.response import JsonResponse
|
from django.http.response import JsonResponse
|
||||||
from django.utils import timezone
|
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from audits.const import LoginStatusChoices
|
from audits.const import LoginStatusChoices
|
||||||
from audits.models import UserLoginLog
|
from audits.models import UserLoginLog
|
||||||
from common.permissions import IsValidLicense
|
from common.permissions import IsValidLicense
|
||||||
from common.utils import lazyproperty
|
from common.utils import lazyproperty
|
||||||
from common.utils.timezone import local_zero_hour, local_now
|
|
||||||
from rbac.permissions import RBACPermission
|
from rbac.permissions import RBACPermission
|
||||||
|
from reports.mixins import DateRangeMixin
|
||||||
|
|
||||||
__all__ = ['UserReportApi']
|
__all__ = ['UserReportApi']
|
||||||
|
|
||||||
|
|
||||||
class UserReportApi(APIView):
|
class UserReportApi(DateRangeMixin, APIView):
|
||||||
http_method_names = ['get']
|
http_method_names = ['get']
|
||||||
# TODO: Define the required RBAC permissions for this API
|
# TODO: Define the required RBAC permissions for this API
|
||||||
rbac_perms = {
|
rbac_perms = {
|
||||||
|
@ -25,28 +24,6 @@ class UserReportApi(APIView):
|
||||||
}
|
}
|
||||||
permission_classes = [RBACPermission, IsValidLicense]
|
permission_classes = [RBACPermission, IsValidLicense]
|
||||||
|
|
||||||
@lazyproperty
|
|
||||||
def days(self):
|
|
||||||
count = self.request.query_params.get('days', 1)
|
|
||||||
return int(count)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def days_to_datetime(self):
|
|
||||||
if self.days == 1:
|
|
||||||
return local_zero_hour()
|
|
||||||
return local_now() - timezone.timedelta(days=self.days)
|
|
||||||
|
|
||||||
@lazyproperty
|
|
||||||
def date_range_list(self):
|
|
||||||
return [
|
|
||||||
(local_now() - timezone.timedelta(days=i)).date()
|
|
||||||
for i in range(self.days - 1, -1, -1)
|
|
||||||
]
|
|
||||||
|
|
||||||
def filter_by_date_range(self, queryset, field_name):
|
|
||||||
date_range_bounds = self.days_to_datetime.date(), (local_now() + timezone.timedelta(days=1)).date()
|
|
||||||
return queryset.filter(**{f'{field_name}__range': date_range_bounds})
|
|
||||||
|
|
||||||
def get_user_login_metrics(self, queryset):
|
def get_user_login_metrics(self, queryset):
|
||||||
filtered_queryset = self.filter_by_date_range(queryset, 'datetime')
|
filtered_queryset = self.filter_by_date_range(queryset, 'datetime')
|
||||||
|
|
||||||
|
@ -103,12 +80,12 @@ class UserReportApi(APIView):
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def user_login_log_queryset(self):
|
def user_login_log_queryset(self):
|
||||||
queryset = UserLoginLog.objects.filter(status=LoginStatusChoices.success)
|
queryset = UserLoginLog.objects.filter(status=LoginStatusChoices.success)
|
||||||
return UserLoginLog.filter_login_queryset_by_org(queryset)
|
return UserLoginLog.filter_queryset_by_org(queryset)
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def user_login_failed_queryset(self):
|
def user_login_failed_queryset(self):
|
||||||
queryset = UserLoginLog.objects.filter(status=LoginStatusChoices.failed)
|
queryset = UserLoginLog.objects.filter(status=LoginStatusChoices.failed)
|
||||||
return UserLoginLog.filter_login_queryset_by_org(queryset)
|
return UserLoginLog.filter_queryset_by_org(queryset)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
data = {}
|
data = {}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
from django.utils import timezone
|
||||||
|
from rest_framework.request import Request
|
||||||
|
|
||||||
|
from common.utils import lazyproperty
|
||||||
|
from common.utils.timezone import local_zero_hour, local_now
|
||||||
|
|
||||||
|
|
||||||
|
class DateRangeMixin:
|
||||||
|
request: Request
|
||||||
|
days_param = 'days'
|
||||||
|
default_days = 1
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def days(self) -> int:
|
||||||
|
raw = self.request.query_params.get(self.days_param, self.default_days)
|
||||||
|
try:
|
||||||
|
return int(raw)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return self.default_days
|
||||||
|
|
||||||
|
@property
|
||||||
|
def start_datetime(self):
|
||||||
|
if self.days == 1:
|
||||||
|
return local_zero_hour()
|
||||||
|
return local_now() - timezone.timedelta(days=self.days)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date_range_bounds(self) -> tuple:
|
||||||
|
start = self.start_datetime.date()
|
||||||
|
end = (local_now() + timezone.timedelta(days=1)).date()
|
||||||
|
return start, end
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def date_range_list(self) -> list:
|
||||||
|
return [
|
||||||
|
(local_now() - timezone.timedelta(days=i)).date()
|
||||||
|
for i in range(self.days - 1, -1, -1)
|
||||||
|
]
|
||||||
|
|
||||||
|
def filter_by_date_range(self, queryset, field_name: str):
|
||||||
|
start, end = self.date_range_bounds
|
||||||
|
return queryset.filter(**{f'{field_name}__range': (start, end)})
|
|
@ -6,5 +6,6 @@ app_name = 'reports'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('reports/', api.ReportViewSet.as_view(), name='report-list'),
|
path('reports/', api.ReportViewSet.as_view(), name='report-list'),
|
||||||
path('reports/users/', api.UserReportApi.as_view(), name='user-list')
|
path('reports/users/', api.UserReportApi.as_view(), name='user-list'),
|
||||||
|
path('reports/user-change-password/', api.UserChangeSecretApi.as_view(), name='user-change-password')
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue