events: add custom manager with helpers for metrics
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		@ -1,13 +1,6 @@
 | 
				
			|||||||
"""authentik administration metrics"""
 | 
					"""authentik administration metrics"""
 | 
				
			||||||
import time
 | 
					 | 
				
			||||||
from collections import Counter
 | 
					 | 
				
			||||||
from datetime import timedelta
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db.models import Count, ExpressionWrapper, F
 | 
					 | 
				
			||||||
from django.db.models.fields import DurationField
 | 
					 | 
				
			||||||
from django.db.models.functions import ExtractHour
 | 
					 | 
				
			||||||
from django.utils.timezone import now
 | 
					 | 
				
			||||||
from drf_spectacular.utils import extend_schema, extend_schema_field
 | 
					from drf_spectacular.utils import extend_schema, extend_schema_field
 | 
				
			||||||
 | 
					from guardian.shortcuts import get_objects_for_user
 | 
				
			||||||
from rest_framework.fields import IntegerField, SerializerMethodField
 | 
					from rest_framework.fields import IntegerField, SerializerMethodField
 | 
				
			||||||
from rest_framework.permissions import IsAdminUser
 | 
					from rest_framework.permissions import IsAdminUser
 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
@ -15,31 +8,7 @@ from rest_framework.response import Response
 | 
				
			|||||||
from rest_framework.views import APIView
 | 
					from rest_framework.views import APIView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.api.utils import PassiveSerializer
 | 
					from authentik.core.api.utils import PassiveSerializer
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import EventAction
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def get_events_per_1h(**filter_kwargs) -> list[dict[str, int]]:
 | 
					 | 
				
			||||||
    """Get event count by hour in the last day, fill with zeros"""
 | 
					 | 
				
			||||||
    date_from = now() - timedelta(days=1)
 | 
					 | 
				
			||||||
    result = (
 | 
					 | 
				
			||||||
        Event.objects.filter(created__gte=date_from, **filter_kwargs)
 | 
					 | 
				
			||||||
        .annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
 | 
					 | 
				
			||||||
        .annotate(age_hours=ExtractHour("age"))
 | 
					 | 
				
			||||||
        .values("age_hours")
 | 
					 | 
				
			||||||
        .annotate(count=Count("pk"))
 | 
					 | 
				
			||||||
        .order_by("age_hours")
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    data = Counter({int(d["age_hours"]): d["count"] for d in result})
 | 
					 | 
				
			||||||
    results = []
 | 
					 | 
				
			||||||
    _now = now()
 | 
					 | 
				
			||||||
    for hour in range(0, -24, -1):
 | 
					 | 
				
			||||||
        results.append(
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                "x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
 | 
					 | 
				
			||||||
                "y_cord": data[hour * -1],
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    return results
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CoordinateSerializer(PassiveSerializer):
 | 
					class CoordinateSerializer(PassiveSerializer):
 | 
				
			||||||
@ -58,12 +27,22 @@ class LoginMetricsSerializer(PassiveSerializer):
 | 
				
			|||||||
    @extend_schema_field(CoordinateSerializer(many=True))
 | 
					    @extend_schema_field(CoordinateSerializer(many=True))
 | 
				
			||||||
    def get_logins_per_1h(self, _):
 | 
					    def get_logins_per_1h(self, _):
 | 
				
			||||||
        """Get successful logins per hour for the last 24 hours"""
 | 
					        """Get successful logins per hour for the last 24 hours"""
 | 
				
			||||||
        return get_events_per_1h(action=EventAction.LOGIN)
 | 
					        user = self.context["user"]
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            get_objects_for_user(user, "authentik_events.view_event")
 | 
				
			||||||
 | 
					            .filter(action=EventAction.LOGIN)
 | 
				
			||||||
 | 
					            .get_events_per_hour()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @extend_schema_field(CoordinateSerializer(many=True))
 | 
					    @extend_schema_field(CoordinateSerializer(many=True))
 | 
				
			||||||
    def get_logins_failed_per_1h(self, _):
 | 
					    def get_logins_failed_per_1h(self, _):
 | 
				
			||||||
        """Get failed logins per hour for the last 24 hours"""
 | 
					        """Get failed logins per hour for the last 24 hours"""
 | 
				
			||||||
        return get_events_per_1h(action=EventAction.LOGIN_FAILED)
 | 
					        user = self.context["user"]
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            get_objects_for_user(user, "authentik_events.view_event")
 | 
				
			||||||
 | 
					            .filter(action=EventAction.LOGIN_FAILED)
 | 
				
			||||||
 | 
					            .get_events_per_hour()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AdministrationMetricsViewSet(APIView):
 | 
					class AdministrationMetricsViewSet(APIView):
 | 
				
			||||||
@ -75,4 +54,5 @@ class AdministrationMetricsViewSet(APIView):
 | 
				
			|||||||
    def get(self, request: Request) -> Response:
 | 
					    def get(self, request: Request) -> Response:
 | 
				
			||||||
        """Login Metrics per 1h"""
 | 
					        """Login Metrics per 1h"""
 | 
				
			||||||
        serializer = LoginMetricsSerializer(True)
 | 
					        serializer = LoginMetricsSerializer(True)
 | 
				
			||||||
 | 
					        serializer.context["user"] = request.user
 | 
				
			||||||
        return Response(serializer.data)
 | 
					        return Response(serializer.data)
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ from django.http.response import HttpResponseBadRequest
 | 
				
			|||||||
from django.shortcuts import get_object_or_404
 | 
					from django.shortcuts import get_object_or_404
 | 
				
			||||||
from drf_spectacular.types import OpenApiTypes
 | 
					from drf_spectacular.types import OpenApiTypes
 | 
				
			||||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
 | 
					from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
 | 
				
			||||||
 | 
					from guardian.shortcuts import get_objects_for_user
 | 
				
			||||||
from rest_framework.decorators import action
 | 
					from rest_framework.decorators import action
 | 
				
			||||||
from rest_framework.fields import ReadOnlyField
 | 
					from rest_framework.fields import ReadOnlyField
 | 
				
			||||||
from rest_framework.parsers import MultiPartParser
 | 
					from rest_framework.parsers import MultiPartParser
 | 
				
			||||||
@ -15,7 +16,7 @@ from rest_framework.viewsets import ModelViewSet
 | 
				
			|||||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
 | 
					from rest_framework_guardian.filters import ObjectPermissionsFilter
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
 | 
					from authentik.admin.api.metrics import CoordinateSerializer
 | 
				
			||||||
from authentik.api.decorators import permission_required
 | 
					from authentik.api.decorators import permission_required
 | 
				
			||||||
from authentik.core.api.providers import ProviderSerializer
 | 
					from authentik.core.api.providers import ProviderSerializer
 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
@ -231,7 +232,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
        app.save()
 | 
					        app.save()
 | 
				
			||||||
        return Response({})
 | 
					        return Response({})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @permission_required("authentik_core.view_application", ["authentik_events.view_event"])
 | 
					    @permission_required("authentik_core.view_application")
 | 
				
			||||||
    @extend_schema(responses={200: CoordinateSerializer(many=True)})
 | 
					    @extend_schema(responses={200: CoordinateSerializer(many=True)})
 | 
				
			||||||
    @action(detail=True, pagination_class=None, filter_backends=[])
 | 
					    @action(detail=True, pagination_class=None, filter_backends=[])
 | 
				
			||||||
    # pylint: disable=unused-argument
 | 
					    # pylint: disable=unused-argument
 | 
				
			||||||
@ -239,8 +240,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
        """Metrics for application logins"""
 | 
					        """Metrics for application logins"""
 | 
				
			||||||
        app = self.get_object()
 | 
					        app = self.get_object()
 | 
				
			||||||
        return Response(
 | 
					        return Response(
 | 
				
			||||||
            get_events_per_1h(
 | 
					            get_objects_for_user(request.user, "authentik_events.view_event")
 | 
				
			||||||
 | 
					            .filter(
 | 
				
			||||||
                action=EventAction.AUTHORIZE_APPLICATION,
 | 
					                action=EventAction.AUTHORIZE_APPLICATION,
 | 
				
			||||||
                context__authorized_application__pk=app.pk.hex,
 | 
					                context__authorized_application__pk=app.pk.hex,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					            .get_events_per_hour()
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
@ -38,7 +38,7 @@ from rest_framework.viewsets import ModelViewSet
 | 
				
			|||||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
 | 
					from rest_framework_guardian.filters import ObjectPermissionsFilter
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
 | 
					from authentik.admin.api.metrics import CoordinateSerializer
 | 
				
			||||||
from authentik.api.decorators import permission_required
 | 
					from authentik.api.decorators import permission_required
 | 
				
			||||||
from authentik.core.api.groups import GroupSerializer
 | 
					from authentik.core.api.groups import GroupSerializer
 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
@ -184,19 +184,31 @@ class UserMetricsSerializer(PassiveSerializer):
 | 
				
			|||||||
    def get_logins_per_1h(self, _):
 | 
					    def get_logins_per_1h(self, _):
 | 
				
			||||||
        """Get successful logins per hour for the last 24 hours"""
 | 
					        """Get successful logins per hour for the last 24 hours"""
 | 
				
			||||||
        user = self.context["user"]
 | 
					        user = self.context["user"]
 | 
				
			||||||
        return get_events_per_1h(action=EventAction.LOGIN, user__pk=user.pk)
 | 
					        return (
 | 
				
			||||||
 | 
					            get_objects_for_user(user, "authentik_events.view_event")
 | 
				
			||||||
 | 
					            .filter(action=EventAction.LOGIN, user__pk=user.pk)
 | 
				
			||||||
 | 
					            .get_events_per_hour()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @extend_schema_field(CoordinateSerializer(many=True))
 | 
					    @extend_schema_field(CoordinateSerializer(many=True))
 | 
				
			||||||
    def get_logins_failed_per_1h(self, _):
 | 
					    def get_logins_failed_per_1h(self, _):
 | 
				
			||||||
        """Get failed logins per hour for the last 24 hours"""
 | 
					        """Get failed logins per hour for the last 24 hours"""
 | 
				
			||||||
        user = self.context["user"]
 | 
					        user = self.context["user"]
 | 
				
			||||||
        return get_events_per_1h(action=EventAction.LOGIN_FAILED, context__username=user.username)
 | 
					        return (
 | 
				
			||||||
 | 
					            get_objects_for_user(user, "authentik_events.view_event")
 | 
				
			||||||
 | 
					            .filter(action=EventAction.LOGIN_FAILED, context__username=user.username)
 | 
				
			||||||
 | 
					            .get_events_per_hour()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @extend_schema_field(CoordinateSerializer(many=True))
 | 
					    @extend_schema_field(CoordinateSerializer(many=True))
 | 
				
			||||||
    def get_authorizations_per_1h(self, _):
 | 
					    def get_authorizations_per_1h(self, _):
 | 
				
			||||||
        """Get failed logins per hour for the last 24 hours"""
 | 
					        """Get failed logins per hour for the last 24 hours"""
 | 
				
			||||||
        user = self.context["user"]
 | 
					        user = self.context["user"]
 | 
				
			||||||
        return get_events_per_1h(action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk)
 | 
					        return (
 | 
				
			||||||
 | 
					            get_objects_for_user(user, "authentik_events.view_event")
 | 
				
			||||||
 | 
					            .filter(action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk)
 | 
				
			||||||
 | 
					            .get_events_per_hour()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UsersFilter(FilterSet):
 | 
					class UsersFilter(FilterSet):
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,6 @@
 | 
				
			|||||||
"""Events API Views"""
 | 
					"""Events API Views"""
 | 
				
			||||||
 | 
					from json import loads
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import django_filters
 | 
					import django_filters
 | 
				
			||||||
from django.db.models.aggregates import Count
 | 
					from django.db.models.aggregates import Count
 | 
				
			||||||
from django.db.models.fields.json import KeyTextTransform
 | 
					from django.db.models.fields.json import KeyTextTransform
 | 
				
			||||||
@ -12,6 +14,7 @@ from rest_framework.response import Response
 | 
				
			|||||||
from rest_framework.serializers import ModelSerializer
 | 
					from rest_framework.serializers import ModelSerializer
 | 
				
			||||||
from rest_framework.viewsets import ModelViewSet
 | 
					from rest_framework.viewsets import ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.admin.api.metrics import CoordinateSerializer
 | 
				
			||||||
from authentik.core.api.utils import PassiveSerializer, TypeCreateSerializer
 | 
					from authentik.core.api.utils import PassiveSerializer, TypeCreateSerializer
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -116,7 +119,6 @@ class EventViewSet(ModelViewSet):
 | 
				
			|||||||
                "action",
 | 
					                "action",
 | 
				
			||||||
                type=OpenApiTypes.STR,
 | 
					                type=OpenApiTypes.STR,
 | 
				
			||||||
                location=OpenApiParameter.QUERY,
 | 
					                location=OpenApiParameter.QUERY,
 | 
				
			||||||
                enum=[action for action in EventAction],
 | 
					 | 
				
			||||||
                required=False,
 | 
					                required=False,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            OpenApiParameter(
 | 
					            OpenApiParameter(
 | 
				
			||||||
@ -124,7 +126,7 @@ class EventViewSet(ModelViewSet):
 | 
				
			|||||||
                type=OpenApiTypes.INT,
 | 
					                type=OpenApiTypes.INT,
 | 
				
			||||||
                location=OpenApiParameter.QUERY,
 | 
					                location=OpenApiParameter.QUERY,
 | 
				
			||||||
                required=False,
 | 
					                required=False,
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    @action(detail=False, methods=["GET"], pagination_class=None)
 | 
					    @action(detail=False, methods=["GET"], pagination_class=None)
 | 
				
			||||||
@ -145,6 +147,40 @@ class EventViewSet(ModelViewSet):
 | 
				
			|||||||
            .order_by("-counted_events")[:top_n]
 | 
					            .order_by("-counted_events")[:top_n]
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @extend_schema(
 | 
				
			||||||
 | 
					        methods=["GET"],
 | 
				
			||||||
 | 
					        responses={200: CoordinateSerializer(many=True)},
 | 
				
			||||||
 | 
					        filters=[],
 | 
				
			||||||
 | 
					        parameters=[
 | 
				
			||||||
 | 
					            OpenApiParameter(
 | 
				
			||||||
 | 
					                "action",
 | 
				
			||||||
 | 
					                type=OpenApiTypes.STR,
 | 
				
			||||||
 | 
					                location=OpenApiParameter.QUERY,
 | 
				
			||||||
 | 
					                required=False,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            OpenApiParameter(
 | 
				
			||||||
 | 
					                "query",
 | 
				
			||||||
 | 
					                type=OpenApiTypes.STR,
 | 
				
			||||||
 | 
					                location=OpenApiParameter.QUERY,
 | 
				
			||||||
 | 
					                required=False,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    @action(detail=False, methods=["GET"], pagination_class=None)
 | 
				
			||||||
 | 
					    def per_month(self, request: Request):
 | 
				
			||||||
 | 
					        """Get the count of events per month"""
 | 
				
			||||||
 | 
					        filtered_action = request.query_params.get("action", EventAction.LOGIN)
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            query = loads(request.query_params.get("query", "{}"))
 | 
				
			||||||
 | 
					        except ValueError:
 | 
				
			||||||
 | 
					            return Response(status=400)
 | 
				
			||||||
 | 
					        return Response(
 | 
				
			||||||
 | 
					            get_objects_for_user(request.user, "authentik_events.view_event")
 | 
				
			||||||
 | 
					            .filter(action=filtered_action)
 | 
				
			||||||
 | 
					            .filter(**query)
 | 
				
			||||||
 | 
					            .get_events_per_day()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @extend_schema(responses={200: TypeCreateSerializer(many=True)})
 | 
					    @extend_schema(responses={200: TypeCreateSerializer(many=True)})
 | 
				
			||||||
    @action(detail=False, pagination_class=None, filter_backends=[])
 | 
					    @action(detail=False, pagination_class=None, filter_backends=[])
 | 
				
			||||||
    def actions(self, request: Request) -> Response:
 | 
					    def actions(self, request: Request) -> Response:
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,6 @@
 | 
				
			|||||||
"""authentik events models"""
 | 
					"""authentik events models"""
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
 | 
					from collections import Counter
 | 
				
			||||||
from datetime import timedelta
 | 
					from datetime import timedelta
 | 
				
			||||||
from inspect import getmodule, stack
 | 
					from inspect import getmodule, stack
 | 
				
			||||||
from smtplib import SMTPException
 | 
					from smtplib import SMTPException
 | 
				
			||||||
@ -7,6 +9,12 @@ from uuid import uuid4
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
 | 
					from django.db.models import Count, ExpressionWrapper, F
 | 
				
			||||||
 | 
					from django.db.models.fields import DurationField
 | 
				
			||||||
 | 
					from django.db.models.functions import ExtractHour
 | 
				
			||||||
 | 
					from django.db.models.functions.datetime import ExtractDay
 | 
				
			||||||
 | 
					from django.db.models.manager import Manager
 | 
				
			||||||
 | 
					from django.db.models.query import QuerySet
 | 
				
			||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
from django.http.request import QueryDict
 | 
					from django.http.request import QueryDict
 | 
				
			||||||
from django.utils.timezone import now
 | 
					from django.utils.timezone import now
 | 
				
			||||||
@ -91,6 +99,72 @@ class EventAction(models.TextChoices):
 | 
				
			|||||||
    CUSTOM_PREFIX = "custom_"
 | 
					    CUSTOM_PREFIX = "custom_"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EventQuerySet(QuerySet):
 | 
				
			||||||
 | 
					    """Custom events query set with helper functions"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_events_per_hour(self) -> list[dict[str, int]]:
 | 
				
			||||||
 | 
					        """Get event count by hour in the last day, fill with zeros"""
 | 
				
			||||||
 | 
					        date_from = now() - timedelta(days=1)
 | 
				
			||||||
 | 
					        result = (
 | 
				
			||||||
 | 
					            self.filter(created__gte=date_from)
 | 
				
			||||||
 | 
					            .annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
 | 
				
			||||||
 | 
					            .annotate(age_hours=ExtractHour("age"))
 | 
				
			||||||
 | 
					            .values("age_hours")
 | 
				
			||||||
 | 
					            .annotate(count=Count("pk"))
 | 
				
			||||||
 | 
					            .order_by("age_hours")
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        data = Counter({int(d["age_hours"]): d["count"] for d in result})
 | 
				
			||||||
 | 
					        results = []
 | 
				
			||||||
 | 
					        _now = now()
 | 
				
			||||||
 | 
					        for hour in range(0, -24, -1):
 | 
				
			||||||
 | 
					            results.append(
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    "x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
 | 
				
			||||||
 | 
					                    "y_cord": data[hour * -1],
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        return results
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_events_per_day(self) -> list[dict[str, int]]:
 | 
				
			||||||
 | 
					        """Get event count by hour in the last day, fill with zeros"""
 | 
				
			||||||
 | 
					        date_from = now() - timedelta(weeks=4)
 | 
				
			||||||
 | 
					        result = (
 | 
				
			||||||
 | 
					            self.filter(created__gte=date_from)
 | 
				
			||||||
 | 
					            .annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
 | 
				
			||||||
 | 
					            .annotate(age_days=ExtractDay("age"))
 | 
				
			||||||
 | 
					            .values("age_days")
 | 
				
			||||||
 | 
					            .annotate(count=Count("pk"))
 | 
				
			||||||
 | 
					            .order_by("age_days")
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        data = Counter({int(d["age_days"]): d["count"] for d in result})
 | 
				
			||||||
 | 
					        results = []
 | 
				
			||||||
 | 
					        _now = now()
 | 
				
			||||||
 | 
					        for day in range(0, -30, -1):
 | 
				
			||||||
 | 
					            results.append(
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    "x_cord": time.mktime((_now + timedelta(days=day)).timetuple()) * 1000,
 | 
				
			||||||
 | 
					                    "y_cord": data[day * -1],
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        return results
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EventManager(Manager):
 | 
				
			||||||
 | 
					    """Custom helper methods for Events"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_queryset(self) -> QuerySet:
 | 
				
			||||||
 | 
					        """use custom queryset"""
 | 
				
			||||||
 | 
					        return EventQuerySet(self.model, using=self._db)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_events_per_hour(self) -> list[dict[str, int]]:
 | 
				
			||||||
 | 
					        """Wrap method from queryset"""
 | 
				
			||||||
 | 
					        return self.get_queryset().get_events_per_hour()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_events_per_day(self) -> list[dict[str, int]]:
 | 
				
			||||||
 | 
					        """Wrap method from queryset"""
 | 
				
			||||||
 | 
					        return self.get_queryset().get_events_per_day()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Event(ExpiringModel):
 | 
					class Event(ExpiringModel):
 | 
				
			||||||
    """An individual Audit/Metrics/Notification/Error Event"""
 | 
					    """An individual Audit/Metrics/Notification/Error Event"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -106,6 +180,8 @@ class Event(ExpiringModel):
 | 
				
			|||||||
    # Shadow the expires attribute from ExpiringModel to override the default duration
 | 
					    # Shadow the expires attribute from ExpiringModel to override the default duration
 | 
				
			||||||
    expires = models.DateTimeField(default=default_event_duration)
 | 
					    expires = models.DateTimeField(default=default_event_duration)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    objects = EventManager()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def _get_app_from_request(request: HttpRequest) -> str:
 | 
					    def _get_app_from_request(request: HttpRequest) -> str:
 | 
				
			||||||
        if not isinstance(request, HttpRequest):
 | 
					        if not isinstance(request, HttpRequest):
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										58
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								schema.yml
									
									
									
									
									
								
							@ -3909,6 +3909,36 @@ paths:
 | 
				
			|||||||
          $ref: '#/components/schemas/ValidationError'
 | 
					          $ref: '#/components/schemas/ValidationError'
 | 
				
			||||||
        '403':
 | 
					        '403':
 | 
				
			||||||
          $ref: '#/components/schemas/GenericError'
 | 
					          $ref: '#/components/schemas/GenericError'
 | 
				
			||||||
 | 
					  /events/events/per_month/:
 | 
				
			||||||
 | 
					    get:
 | 
				
			||||||
 | 
					      operationId: events_events_per_month_list
 | 
				
			||||||
 | 
					      description: Get the count of events per month
 | 
				
			||||||
 | 
					      parameters:
 | 
				
			||||||
 | 
					      - in: query
 | 
				
			||||||
 | 
					        name: action
 | 
				
			||||||
 | 
					        schema:
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					      - in: query
 | 
				
			||||||
 | 
					        name: query
 | 
				
			||||||
 | 
					        schema:
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					      tags:
 | 
				
			||||||
 | 
					      - events
 | 
				
			||||||
 | 
					      security:
 | 
				
			||||||
 | 
					      - authentik: []
 | 
				
			||||||
 | 
					      responses:
 | 
				
			||||||
 | 
					        '200':
 | 
				
			||||||
 | 
					          content:
 | 
				
			||||||
 | 
					            application/json:
 | 
				
			||||||
 | 
					              schema:
 | 
				
			||||||
 | 
					                type: array
 | 
				
			||||||
 | 
					                items:
 | 
				
			||||||
 | 
					                  $ref: '#/components/schemas/Coordinate'
 | 
				
			||||||
 | 
					          description: ''
 | 
				
			||||||
 | 
					        '400':
 | 
				
			||||||
 | 
					          $ref: '#/components/schemas/ValidationError'
 | 
				
			||||||
 | 
					        '403':
 | 
				
			||||||
 | 
					          $ref: '#/components/schemas/GenericError'
 | 
				
			||||||
  /events/events/top_per_user/:
 | 
					  /events/events/top_per_user/:
 | 
				
			||||||
    get:
 | 
					    get:
 | 
				
			||||||
      operationId: events_events_top_per_user_list
 | 
					      operationId: events_events_top_per_user_list
 | 
				
			||||||
@ -3918,34 +3948,6 @@ paths:
 | 
				
			|||||||
        name: action
 | 
					        name: action
 | 
				
			||||||
        schema:
 | 
					        schema:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
          enum:
 | 
					 | 
				
			||||||
          - authorize_application
 | 
					 | 
				
			||||||
          - configuration_error
 | 
					 | 
				
			||||||
          - custom_
 | 
					 | 
				
			||||||
          - email_sent
 | 
					 | 
				
			||||||
          - flow_execution
 | 
					 | 
				
			||||||
          - impersonation_ended
 | 
					 | 
				
			||||||
          - impersonation_started
 | 
					 | 
				
			||||||
          - invitation_used
 | 
					 | 
				
			||||||
          - login
 | 
					 | 
				
			||||||
          - login_failed
 | 
					 | 
				
			||||||
          - logout
 | 
					 | 
				
			||||||
          - model_created
 | 
					 | 
				
			||||||
          - model_deleted
 | 
					 | 
				
			||||||
          - model_updated
 | 
					 | 
				
			||||||
          - password_set
 | 
					 | 
				
			||||||
          - policy_exception
 | 
					 | 
				
			||||||
          - policy_execution
 | 
					 | 
				
			||||||
          - property_mapping_exception
 | 
					 | 
				
			||||||
          - secret_rotate
 | 
					 | 
				
			||||||
          - secret_view
 | 
					 | 
				
			||||||
          - source_linked
 | 
					 | 
				
			||||||
          - suspicious_request
 | 
					 | 
				
			||||||
          - system_exception
 | 
					 | 
				
			||||||
          - system_task_exception
 | 
					 | 
				
			||||||
          - system_task_execution
 | 
					 | 
				
			||||||
          - update_available
 | 
					 | 
				
			||||||
          - user_write
 | 
					 | 
				
			||||||
      - in: query
 | 
					      - in: query
 | 
				
			||||||
        name: top_n
 | 
					        name: top_n
 | 
				
			||||||
        schema:
 | 
					        schema:
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user