diff --git a/auditlog/middleware.py b/auditlog/middleware.py index c27f485..1660332 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -1,5 +1,6 @@ from django.conf import settings from django.contrib.auth import get_user_model +from django.utils.functional import SimpleLazyObject from auditlog.cid import set_cid from auditlog.context import set_extra_data @@ -59,7 +60,9 @@ class AuditlogMiddleware: context_data["remote_addr"] = self._get_remote_addr(request) context_data["remote_port"] = self._get_remote_port(request) - context_data["actor"] = self._get_actor(request) + # SimpleLazyObject defers evaluating the user in the request until it is accessed + # so it prevents a bug where the default anonymous user is used instead of the authenticated user + context_data["actor"] = SimpleLazyObject(lambda: self._get_actor(request)) return context_data diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 6764195..583f65f 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -723,6 +723,36 @@ class MiddlewareTest(TestCase): self.assertEqual(self.middleware._get_actor(request), actor) + def test_lazy_actor_resolution_with_deferred_auth(self): + """ + When authentication is deferred (e.g. DRF token auth), request.user + may still be AnonymousUser at the time the middleware calls + get_extra_data(). Using SimpleLazyObject ensures the actor is resolved + at model-save time, when request.user has been updated. + """ + request = self.factory.get("/") + request.user = AnonymousUser() + + def get_response(req): + # Simulate deferred auth setting the real user after middleware ran + req.user = self.user + SimpleModel.objects.create(text="I am not difficult.") + return self.response_mock + + self.get_response_mock.side_effect = get_response + + self.middleware(request) + + history = SimpleModel.objects.last().history.get( + action=LogEntry.Action.CREATE + ) + self.assertEqual( + history.actor, + self.user, + msg="Actor should be resolved lazily to the authenticated user, " + "not eagerly to None (AnonymousUser)", + ) + class SimpleIncludeModelTest(TestCase): """Log only changes in include_fields"""