Skip to content

Commit 9bed8e3

Browse files
MartinCastroAlvarezmartin-castro-laminr-aiclaude
authored
fix(api): move recent-actions LogEntry query out of api/ (S-15) (#523)
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: Martin Castro Laminrs <mcastro@laminr.ai> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ae6bec8 commit 9bed8e3

2 files changed

Lines changed: 21 additions & 5 deletions

File tree

django_admin_react/api/views/recent_actions.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from django_admin_react.api.permissions import forbidden_response
3030
from django_admin_react.api.permissions import is_admin_user
3131
from django_admin_react.api.registry import get_admin_site
32+
from django_admin_react.audit import recent_actions_for_user
3233

3334
# Default / ceiling for the number of entries returned. Django's index
3435
# 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:
6061

6162
# ``is_admin_user`` guarantees an authenticated user, so pk is set
6263
# (it may be int or str for a custom user model — both are valid
63-
# lookups). Scoped to this user only — Django's own index panel
64-
# filters the same way.
64+
# lookups). Scoped to this user only; the LogEntry query lives in
65+
# ``audit.py`` (outside ``api/``), the designated home for the
66+
# framework audit table — see that module's docstring.
6567
user_pk = cast("str | int", request.user.pk)
66-
entries = list(
67-
LogEntry.objects.filter(user__pk=user_pk).order_by("-action_time")[: _limit(request)]
68-
)
68+
entries = list(recent_actions_for_user(user_pk, _limit(request)))
6969
body = {"actions": [_serialize_action(e, admin_site, request) for e in entries]}
7070
response = JsonResponse(body, status=200)
7171
response["Cache-Control"] = "no-store"

django_admin_react/audit.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
1818
- :func:`object_log_entries` — the ``LogEntry`` queryset for one object,
1919
newest-first, with the acting user pre-fetched.
20+
- :func:`recent_actions_for_user` — the most recent ``LogEntry`` rows for
21+
one user (the index "Recent actions" panel), newest-first.
2022
"""
2123

2224
from __future__ import annotations
@@ -40,3 +42,17 @@ def object_log_entries(obj: Model) -> QuerySet[LogEntry]:
4042
.select_related("user")
4143
.order_by("-action_time")
4244
)
45+
46+
47+
def recent_actions_for_user(user_pk: str | int, limit: int) -> QuerySet[LogEntry]:
48+
"""Return the most recent ``LogEntry`` rows for one user, newest first.
49+
50+
The user-scoped counterpart of :func:`object_log_entries`: filtered by
51+
the acting user and capped at ``limit`` — exactly how Django's admin
52+
index "Recent actions" panel reads the log
53+
(``LogEntry.objects.filter(user=...)``). Same get_queryset-rule
54+
rationale as the module docstring: LogEntry is a framework audit
55+
table, not a consumer model, so it is read directly here (outside
56+
``api/``) rather than via ``ModelAdmin.get_queryset``.
57+
"""
58+
return LogEntry.objects.filter(user__pk=user_pk).order_by("-action_time")[:limit]

0 commit comments

Comments
 (0)