diff --git a/django_admin_react/api/views/recent_actions.py b/django_admin_react/api/views/recent_actions.py index 05d1176..9b21901 100644 --- a/django_admin_react/api/views/recent_actions.py +++ b/django_admin_react/api/views/recent_actions.py @@ -29,6 +29,7 @@ from django_admin_react.api.permissions import forbidden_response from django_admin_react.api.permissions import is_admin_user from django_admin_react.api.registry import get_admin_site +from django_admin_react.audit import user_log_entries # Default / ceiling for the number of entries returned. Django's index # shows 10; the ceiling keeps a hand-crafted ``?limit=`` from scanning @@ -60,12 +61,13 @@ def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: # ``is_admin_user`` guarantees an authenticated user, so pk is set # (it may be int or str for a custom user model — both are valid - # lookups). Scoped to this user only — Django's own index panel - # filters the same way. + # lookups). The LogEntry query lives in ``django_admin_react.audit`` + # (outside ``api/``): LogEntry is Django's own audit table, not a + # consumer model, so the get_queryset rule is inapplicable — and + # keeping ``.objects.filter`` out of ``api/`` honours SECURITY §3 + # rule 10 at the file-system level (see audit.py + test_s15). user_pk = cast("str | int", request.user.pk) - entries = list( - LogEntry.objects.filter(user__pk=user_pk).order_by("-action_time")[: _limit(request)] - ) + entries = user_log_entries(user_pk, _limit(request)) body = {"actions": [_serialize_action(e, admin_site, request) for e in entries]} response = JsonResponse(body, status=200) response["Cache-Control"] = "no-store" diff --git a/django_admin_react/audit.py b/django_admin_react/audit.py index 27f8dfe..379f99f 100644 --- a/django_admin_react/audit.py +++ b/django_admin_react/audit.py @@ -17,6 +17,8 @@ - :func:`object_log_entries` — the ``LogEntry`` queryset for one object, newest-first, with the acting user pre-fetched. +- :func:`user_log_entries` — a user's own recent ``LogEntry`` rows + (the index "Recent actions" feed), newest-first. """ from __future__ import annotations @@ -40,3 +42,17 @@ def object_log_entries(obj: Model) -> QuerySet[LogEntry]: .select_related("user") .order_by("-action_time") ) + + +def user_log_entries(user_pk: str | int, limit: int) -> list[LogEntry]: + """Return ``user_pk``'s most recent ``LogEntry`` rows, newest first. + + Backs the index "Recent actions" panel — the same per-user scope + Django's ``AdminSite.index`` applies + (``LogEntry.objects.filter(user=request.user)``). ``limit`` is sliced + in the database; callers must gate the request first and pass an + authenticated user's pk (int or str, per the user model). The + ``user`` filter is the security boundary: the feed never surfaces + another user's actions. + """ + return list(LogEntry.objects.filter(user__pk=user_pk).order_by("-action_time")[:limit])