Repository: goauthentik/authentik

Issue:
User write stages do not trigger a Model Updated event when writing to an existing user
**Describe the bug**
User write stages do not trigger a "Model updated" event when the user is existing if the stage is after an email stage with "activate pending user on success".

**To Reproduce**
Steps to reproduce the behavior:
1. Create a new enrollment flow
2. Add a prompt stage to enter username email and name
3. Do a user write, make sure it always creates a new user and store the user as inactive
4. Add an email stage, and enable "Activate pending user on success"
5. Add a prompt stage to set the password
6. Do another user write, make sure it never creates a user
7. Do a log in

During this process there is no Model Updated event sent in the second user write.
However, when you remove the email stage, it will trigger this event.
It also triggers the correct event in the user-write on a user settings flow for example, so it seems to be related to the activation stage.

**Expected behavior**
A user write should trigger a Model Updated event when modifying an existing user, as it also triggers a Model Created event when a new user is written to.

**Version and Deployment (please complete the following information):**
 - authentik version: 2023.1.2



Discussion:
And it seems that an email stage that has "Activate pending user on success" enabled, which writes to the user, also does not trigger a "Model updated" event. In fact, it doesn't trigger any event at all.

Edit: it seems the main issue is also caused by the email stage with activation. When I remove that from enrollment, the user write will correctly send a "Model updated" event. 

I updated the original description.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.


Resolving patch:
diff --git a/authentik/events/middleware.py b/authentik/events/middleware.py
--- a/authentik/events/middleware.py
+++ b/authentik/events/middleware.py
@@ -93,21 +93,30 @@ class AuditMiddleware:
     of models"""
 
     get_response: Callable[[HttpRequest], HttpResponse]
+    anonymous_user: User = None
 
     def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
         self.get_response = get_response
 
+    def _ensure_fallback_user(self):
+        """Defer fetching anonymous user until we have to"""
+        if self.anonymous_user:
+            return
+        from guardian.shortcuts import get_anonymous_user
+
+        self.anonymous_user = get_anonymous_user()
+
     def connect(self, request: HttpRequest):
         """Connect signal for automatic logging"""
-        if not hasattr(request, "user"):
-            return
-        if not getattr(request.user, "is_authenticated", False):
-            return
+        self._ensure_fallback_user()
+        user = getattr(request, "user", self.anonymous_user)
+        if not user.is_authenticated:
+            user = self.anonymous_user
         if not hasattr(request, "request_id"):
             return
-        post_save_handler = partial(self.post_save_handler, user=request.user, request=request)
-        pre_delete_handler = partial(self.pre_delete_handler, user=request.user, request=request)
-        m2m_changed_handler = partial(self.m2m_changed_handler, user=request.user, request=request)
+        post_save_handler = partial(self.post_save_handler, user=user, request=request)
+        pre_delete_handler = partial(self.pre_delete_handler, user=user, request=request)
+        m2m_changed_handler = partial(self.m2m_changed_handler, user=user, request=request)
         post_save.connect(
             post_save_handler,
             dispatch_uid=request.request_id,


Test patch:
diff --git a/authentik/stages/user_write/tests.py b/authentik/stages/user_write/tests.py
--- a/authentik/stages/user_write/tests.py
+++ b/authentik/stages/user_write/tests.py
@@ -6,6 +6,7 @@
 from authentik.core.models import USER_ATTRIBUTE_SOURCES, Group, Source, User, UserSourceConnection
 from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION
 from authentik.core.tests.utils import create_test_admin_user, create_test_flow
+from authentik.events.models import Event, EventAction
 from authentik.flows.markers import StageMarker
 from authentik.flows.models import FlowStageBinding
 from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
@@ -58,11 +59,33 @@ def test_user_create(self):
         self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
         user_qs = User.objects.filter(username=plan.context[PLAN_CONTEXT_PROMPT]["username"])
         self.assertTrue(user_qs.exists())
-        self.assertTrue(user_qs.first().check_password(password))
-        self.assertEqual(
-            list(user_qs.first().ak_groups.order_by("name")), [self.other_group, self.group]
+        user = user_qs.first()
+        self.assertTrue(user.check_password(password))
+        self.assertEqual(list(user.ak_groups.order_by("name")), [self.other_group, self.group])
+        self.assertEqual(user.attributes, {USER_ATTRIBUTE_SOURCES: [self.source.name]})
+
+        self.assertTrue(
+            Event.objects.filter(
+                action=EventAction.MODEL_CREATED,
+                context__model={
+                    "app": "authentik_core",
+                    "model_name": "user",
+                    "pk": user.pk,
+                    "name": "name",
+                },
+            )
+        )
+        self.assertTrue(
+            Event.objects.filter(
+                action=EventAction.MODEL_UPDATED,
+                context__model={
+                    "app": "authentik_core",
+                    "model_name": "user",
+                    "pk": user.pk,
+                    "name": "name",
+                },
+            )
         )
-        self.assertEqual(user_qs.first().attributes, {USER_ATTRIBUTE_SOURCES: [self.source.name]})
 
     def test_user_update(self):
         """Test update of existing user"""