perf: user change password

pull/15630/merge^2
feng 2025-07-19 15:07:15 +08:00
parent 7f46f32200
commit dab97ea16c
7 changed files with 145 additions and 36 deletions

View File

@ -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):

View File

@ -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

View File

@ -1 +1,2 @@
from .change_password import *
from .user import * from .user import *

View File

@ -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)

View File

@ -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 = {}

42
apps/reports/mixins.py Normal file
View File

@ -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)})

View File

@ -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')
] ]