From 6ebe8b8565a18f81efec7daba21116b16d0caa4b Mon Sep 17 00:00:00 2001 From: Martin Castro Laminrs Date: Thu, 28 May 2026 01:25:05 +0200 Subject: [PATCH] fix(api): move recent-actions LogEntry query out of api/ (S-15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recent-actions view (#502) queried LogEntry.objects.filter(user=…) inline, tripping the S-15 security guard (no Model.objects.all/filter in api/). CI didn't catch it — the test suite doesn't gate CI yet (#452), only CodeQL does — so it landed on main and the full local run flagged it. Move the user-scoped LogEntry query into django_admin_react/audit.py as recent_actions_for_user(), alongside object_log_entries(). That module is the designated, documented home for framework audit-table access outside api/ (LogEntry is not a consumer model, so the get_queryset rule is categorically inapplicable). The view now delegates to it — no behaviour change, S-15 satisfied honestly. Co-Authored-By: Claude Opus 4.7 (1M context) --- django_admin_react/api/views/recent_actions.py | 10 +++++----- django_admin_react/audit.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/django_admin_react/api/views/recent_actions.py b/django_admin_react/api/views/recent_actions.py index 05d1176b..2991dfc0 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 recent_actions_for_user # 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,11 @@ 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). Scoped to this user only; the LogEntry query lives in + # ``audit.py`` (outside ``api/``), the designated home for the + # framework audit table — see that module's docstring. user_pk = cast("str | int", request.user.pk) - entries = list( - LogEntry.objects.filter(user__pk=user_pk).order_by("-action_time")[: _limit(request)] - ) + entries = list(recent_actions_for_user(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 27f8dfef..05f5c265 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:`recent_actions_for_user` — the most recent ``LogEntry`` rows for + one user (the index "Recent actions" panel), 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 recent_actions_for_user(user_pk: str | int, limit: int) -> QuerySet[LogEntry]: + """Return the most recent ``LogEntry`` rows for one user, newest first. + + The user-scoped counterpart of :func:`object_log_entries`: filtered by + the acting user and capped at ``limit`` — exactly how Django's admin + index "Recent actions" panel reads the log + (``LogEntry.objects.filter(user=...)``). Same get_queryset-rule + rationale as the module docstring: LogEntry is a framework audit + table, not a consumer model, so it is read directly here (outside + ``api/``) rather than via ``ModelAdmin.get_queryset``. + """ + return LogEntry.objects.filter(user__pk=user_pk).order_by("-action_time")[:limit]