We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Unable to use Date prompt type in enrollment flow.
**Describe the bug**
I wanted to add a Date prompt in the enrollment flow, asking the user to provide their birthdate (`attributes.birthday`), just for verification purpose. But, after I added it to the flow and bind the prompt to a stage, when user filled their birthdate info in and click "Continue", authentik returned with the error "Request has been denied. Failed to update user. Please try again later.".

**To Reproduce**
Steps to reproduce the behavior:

1. Create a new prompt with Date format.
2. Bind that prompt to a prompt stage in an enrollment flow.
3. Go to the enrollment flow and fill all the information.
4. See error.

**Expected behavior**
The user will be written to authentik, and the verification email should be sent to the user.

**Screenshots**
![image](https://github.com/goauthentik/authentik/assets/73780382/4b7ccd75-a053-475b-bbf6-8001eb7e1f40)
![image](https://github.com/goauthentik/authentik/assets/73780382/d6828a2c-a10f-4a10-981a-f5a6a3f78880)
![image](https://github.com/goauthentik/authentik/assets/73780382/d2056849-498e-49bd-9c22-40042811a1ef)
![image](https://github.com/goauthentik/authentik/assets/73780382/eff8a039-936d-4150-be00-6d9d1287e6b2)



**Logs**
From Server:
```
{"auth_via": "unauthenticated", "domain_url": "domain", "event": "/api/v3/flows/executor/nzid-register/?query=next%3D%252F", "host": "domain", "level": "info", "logger": "authentik.asgi", "method": "GET", "pid": 44, "remote": "x.x.x.x", "request_id": "94ca682b026941abaa8b853b09380512", "runtime": 610, "schema_name": "public", "scheme": "https", "status": 200, "timestamp": "2024-02-27T18:41:10.517363", "user": "", "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"}
```
I can't find the logs from worker (LOG_LEVEL already TRACE). Will send the logs asap.

**Version and Deployment (please complete the following information):**

-   authentik version: [2024.2.1](https://goauthentik.io/docs/releases/2024.2)
-   Deployment: docker-compose

**Additional context**
I tested the flow using Text type. It worked, but I still want client to render a date picker to user for easier input.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `authentik/stages/user_write/stage.py`
Content:
```
1 """Write stage logic"""
2 
3 from typing import Any
4 
5 from django.contrib.auth import update_session_auth_hash
6 from django.db import transaction
7 from django.db.utils import IntegrityError, InternalError
8 from django.http import HttpRequest, HttpResponse
9 from django.utils.translation import gettext as _
10 from rest_framework.exceptions import ValidationError
11 
12 from authentik.core.middleware import SESSION_KEY_IMPERSONATE_USER
13 from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection, UserTypes
14 from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION
15 from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
16 from authentik.flows.stage import StageView
17 from authentik.flows.views.executor import FlowExecutorView
18 from authentik.lib.config import set_path_in_dict
19 from authentik.stages.password import BACKEND_INBUILT
20 from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
21 from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
22 from authentik.stages.user_write.models import UserCreationMode
23 from authentik.stages.user_write.signals import user_write
24 
25 PLAN_CONTEXT_GROUPS = "groups"
26 PLAN_CONTEXT_USER_TYPE = "user_type"
27 PLAN_CONTEXT_USER_PATH = "user_path"
28 
29 
30 class UserWriteStageView(StageView):
31     """Finalise Enrollment flow by creating a user object."""
32 
33     def __init__(self, executor: FlowExecutorView, **kwargs):
34         super().__init__(executor, **kwargs)
35         self.disallowed_user_attributes = [
36             "groups",
37         ]
38 
39     @staticmethod
40     def write_attribute(user: User, key: str, value: Any):
41         """Allow use of attributes.foo.bar when writing to a user, with full
42         recursion"""
43         parts = key.replace("_", ".").split(".")
44         if len(parts) < 1:  # pragma: no cover
45             return
46         # Function will always be called with a key like attributes.
47         # this is just a sanity check to ensure that is removed
48         if parts[0] == "attributes":
49             parts = parts[1:]
50         set_path_in_dict(user.attributes, ".".join(parts), value)
51 
52     def ensure_user(self) -> tuple[User | None, bool]:
53         """Ensure a user exists"""
54         user_created = False
55         path = self.executor.plan.context.get(
56             PLAN_CONTEXT_USER_PATH, self.executor.current_stage.user_path_template
57         )
58         if path == "":
59             path = User.default_path()
60 
61         try:
62             user_type = UserTypes(
63                 self.executor.plan.context.get(
64                     PLAN_CONTEXT_USER_TYPE,
65                     self.executor.current_stage.user_type,
66                 )
67             )
68         except ValueError:
69             user_type = self.executor.current_stage.user_type
70         if user_type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
71             user_type = UserTypes.SERVICE_ACCOUNT
72 
73         if not self.request.user.is_anonymous:
74             self.executor.plan.context.setdefault(PLAN_CONTEXT_PENDING_USER, self.request.user)
75         if (
76             PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context
77             or self.executor.current_stage.user_creation_mode == UserCreationMode.ALWAYS_CREATE
78         ):
79             if self.executor.current_stage.user_creation_mode == UserCreationMode.NEVER_CREATE:
80                 return None, False
81             self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(
82                 is_active=not self.executor.current_stage.create_users_as_inactive,
83                 path=path,
84                 type=user_type,
85             )
86             self.executor.plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT
87             self.logger.debug(
88                 "Created new user",
89                 flow_slug=self.executor.flow.slug,
90             )
91             user_created = True
92         user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
93         return user, user_created
94 
95     def update_user(self, user: User):
96         """Update `user` with data from plan context
97 
98         Only simple attributes are updated, nothing which requires a foreign key or m2m"""
99         data: dict = self.executor.plan.context[PLAN_CONTEXT_PROMPT]
100         # This is always sent back but not written to the user
101         data.pop("component", None)
102         for key, value in data.items():
103             setter_name = f"set_{key}"
104             # Check if user has a setter for this key, like set_password
105             if hasattr(user, setter_name):
106                 setter = getattr(user, setter_name)
107                 if callable(setter):
108                     setter(value)
109             elif key in self.disallowed_user_attributes:
110                 self.logger.info("discarding key", key=key)
111                 continue
112             # For exact attributes match, update the dictionary in place
113             elif key == "attributes":
114                 user.attributes.update(value)
115             # If using dot notation, use the correct helper to update the nested value
116             elif key.startswith("attributes.") or key.startswith("attributes_"):
117                 UserWriteStageView.write_attribute(user, key, value)
118             # User has this key already
119             elif hasattr(user, key):
120                 setattr(user, key, value)
121             # If none of the cases above matched, we have an attribute that the user doesn't have,
122             # has no setter for, is not a nested attributes value and as such is invalid
123             else:
124                 self.logger.info("discarding key", key=key)
125                 continue
126         # Check if we're writing from a source, and save the source to the attributes
127         if PLAN_CONTEXT_SOURCES_CONNECTION in self.executor.plan.context:
128             if USER_ATTRIBUTE_SOURCES not in user.attributes or not isinstance(
129                 user.attributes.get(USER_ATTRIBUTE_SOURCES), list
130             ):
131                 user.attributes[USER_ATTRIBUTE_SOURCES] = []
132             connection: UserSourceConnection = self.executor.plan.context[
133                 PLAN_CONTEXT_SOURCES_CONNECTION
134             ]
135             if connection.source.name not in user.attributes[USER_ATTRIBUTE_SOURCES]:
136                 user.attributes[USER_ATTRIBUTE_SOURCES].append(connection.source.name)
137 
138     def dispatch(self, request: HttpRequest) -> HttpResponse:
139         """Save data in the current flow to the currently pending user. If no user is pending,
140         a new user is created."""
141         if PLAN_CONTEXT_PROMPT not in self.executor.plan.context:
142             message = _("No Pending data.")
143             self.logger.debug(message)
144             return self.executor.stage_invalid(message)
145         data = self.executor.plan.context[PLAN_CONTEXT_PROMPT]
146         user, user_created = self.ensure_user()
147         if not user:
148             message = _("No user found and can't create new user.")
149             self.logger.info(message)
150             return self.executor.stage_invalid(message)
151         # Before we change anything, check if the user is the same as in the request
152         # and we're updating a password. In that case we need to update the session hash
153         # Also check that we're not currently impersonating, so we don't update the session
154         should_update_session = False
155         if (
156             any("password" in x for x in data.keys())
157             and self.request.user.pk == user.pk
158             and SESSION_KEY_IMPERSONATE_USER not in self.request.session
159         ):
160             should_update_session = True
161         try:
162             self.update_user(user)
163         except ValidationError as exc:
164             self.logger.warning("failed to update user", exc=exc)
165             return self.executor.stage_invalid(_("Failed to update user. Please try again later."))
166         # Extra check to prevent flows from saving a user with a blank username
167         if user.username == "":
168             self.logger.warning("Aborting write to empty username", user=user)
169             return self.executor.stage_invalid()
170         try:
171             with transaction.atomic():
172                 user.save()
173                 if self.executor.current_stage.create_users_group:
174                     user.ak_groups.add(self.executor.current_stage.create_users_group)
175                 if PLAN_CONTEXT_GROUPS in self.executor.plan.context:
176                     user.ak_groups.add(*self.executor.plan.context[PLAN_CONTEXT_GROUPS])
177         except (IntegrityError, ValueError, TypeError, InternalError) as exc:
178             self.logger.warning("Failed to save user", exc=exc)
179             return self.executor.stage_invalid(_("Failed to update user. Please try again later."))
180         user_write.send(sender=self, request=request, user=user, data=data, created=user_created)
181         # Check if the password has been updated, and update the session auth hash
182         if should_update_session:
183             update_session_auth_hash(self.request, user)
184             self.logger.debug("Updated session hash", user=user)
185         self.logger.debug(
186             "Updated existing user",
187             user=user,
188             flow_slug=self.executor.flow.slug,
189         )
190         return self.executor.stage_ok()
191
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/authentik/stages/user_write/stage.py b/authentik/stages/user_write/stage.py
--- a/authentik/stages/user_write/stage.py
+++ b/authentik/stages/user_write/stage.py
@@ -12,6 +12,7 @@
 from authentik.core.middleware import SESSION_KEY_IMPERSONATE_USER
 from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection, UserTypes
 from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION
+from authentik.events.utils import sanitize_item
 from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
 from authentik.flows.stage import StageView
 from authentik.flows.views.executor import FlowExecutorView
@@ -47,7 +48,7 @@
         # this is just a sanity check to ensure that is removed
         if parts[0] == "attributes":
             parts = parts[1:]
-        set_path_in_dict(user.attributes, ".".join(parts), value)
+        set_path_in_dict(user.attributes, ".".join(parts), sanitize_item(value))
 
     def ensure_user(self) -> tuple[User | None, bool]:
         """Ensure a user exists"""

{"golden_diff": "diff --git a/authentik/stages/user_write/stage.py b/authentik/stages/user_write/stage.py\n--- a/authentik/stages/user_write/stage.py\n+++ b/authentik/stages/user_write/stage.py\n@@ -12,6 +12,7 @@\n from authentik.core.middleware import SESSION_KEY_IMPERSONATE_USER\n from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection, UserTypes\n from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION\n+from authentik.events.utils import sanitize_item\n from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER\n from authentik.flows.stage import StageView\n from authentik.flows.views.executor import FlowExecutorView\n@@ -47,7 +48,7 @@\n         # this is just a sanity check to ensure that is removed\n         if parts[0] == \"attributes\":\n             parts = parts[1:]\n-        set_path_in_dict(user.attributes, \".\".join(parts), value)\n+        set_path_in_dict(user.attributes, \".\".join(parts), sanitize_item(value))\n \n     def ensure_user(self) -> tuple[User | None, bool]:\n         \"\"\"Ensure a user exists\"\"\"\n", "issue": "Unable to use Date prompt type in enrollment flow.\n**Describe the bug**\r\nI wanted to add a Date prompt in the enrollment flow, asking the user to provide their birthdate (`attributes.birthday`), just for verification purpose. But, after I added it to the flow and bind the prompt to a stage, when user filled their birthdate info in and click \"Continue\", authentik returned with the error \"Request has been denied. Failed to update user. Please try again later.\".\r\n\r\n**To Reproduce**\r\nSteps to reproduce the behavior:\r\n\r\n1. Create a new prompt with Date format.\r\n2. Bind that prompt to a prompt stage in an enrollment flow.\r\n3. Go to the enrollment flow and fill all the information.\r\n4. See error.\r\n\r\n**Expected behavior**\r\nThe user will be written to authentik, and the verification email should be sent to the user.\r\n\r\n**Screenshots**\r\n![image](https://github.com/goauthentik/authentik/assets/73780382/4b7ccd75-a053-475b-bbf6-8001eb7e1f40)\r\n![image](https://github.com/goauthentik/authentik/assets/73780382/d6828a2c-a10f-4a10-981a-f5a6a3f78880)\r\n![image](https://github.com/goauthentik/authentik/assets/73780382/d2056849-498e-49bd-9c22-40042811a1ef)\r\n![image](https://github.com/goauthentik/authentik/assets/73780382/eff8a039-936d-4150-be00-6d9d1287e6b2)\r\n\r\n\r\n\r\n**Logs**\r\nFrom Server:\r\n```\r\n{\"auth_via\": \"unauthenticated\", \"domain_url\": \"domain\", \"event\": \"/api/v3/flows/executor/nzid-register/?query=next%3D%252F\", \"host\": \"domain\", \"level\": \"info\", \"logger\": \"authentik.asgi\", \"method\": \"GET\", \"pid\": 44, \"remote\": \"x.x.x.x\", \"request_id\": \"94ca682b026941abaa8b853b09380512\", \"runtime\": 610, \"schema_name\": \"public\", \"scheme\": \"https\", \"status\": 200, \"timestamp\": \"2024-02-27T18:41:10.517363\", \"user\": \"\", \"user_agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36\"}\r\n```\r\nI can't find the logs from worker (LOG_LEVEL already TRACE). Will send the logs asap.\r\n\r\n**Version and Deployment (please complete the following information):**\r\n\r\n-   authentik version: [2024.2.1](https://goauthentik.io/docs/releases/2024.2)\r\n-   Deployment: docker-compose\r\n\r\n**Additional context**\r\nI tested the flow using Text type. It worked, but I still want client to render a date picker to user for easier input.\r\n\n", "before_files": [{"content": "\"\"\"Write stage logic\"\"\"\n\nfrom typing import Any\n\nfrom django.contrib.auth import update_session_auth_hash\nfrom django.db import transaction\nfrom django.db.utils import IntegrityError, InternalError\nfrom django.http import HttpRequest, HttpResponse\nfrom django.utils.translation import gettext as _\nfrom rest_framework.exceptions import ValidationError\n\nfrom authentik.core.middleware import SESSION_KEY_IMPERSONATE_USER\nfrom authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection, UserTypes\nfrom authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION\nfrom authentik.flows.planner import PLAN_CONTEXT_PENDING_USER\nfrom authentik.flows.stage import StageView\nfrom authentik.flows.views.executor import FlowExecutorView\nfrom authentik.lib.config import set_path_in_dict\nfrom authentik.stages.password import BACKEND_INBUILT\nfrom authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND\nfrom authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT\nfrom authentik.stages.user_write.models import UserCreationMode\nfrom authentik.stages.user_write.signals import user_write\n\nPLAN_CONTEXT_GROUPS = \"groups\"\nPLAN_CONTEXT_USER_TYPE = \"user_type\"\nPLAN_CONTEXT_USER_PATH = \"user_path\"\n\n\nclass UserWriteStageView(StageView):\n    \"\"\"Finalise Enrollment flow by creating a user object.\"\"\"\n\n    def __init__(self, executor: FlowExecutorView, **kwargs):\n        super().__init__(executor, **kwargs)\n        self.disallowed_user_attributes = [\n            \"groups\",\n        ]\n\n    @staticmethod\n    def write_attribute(user: User, key: str, value: Any):\n        \"\"\"Allow use of attributes.foo.bar when writing to a user, with full\n        recursion\"\"\"\n        parts = key.replace(\"_\", \".\").split(\".\")\n        if len(parts) < 1:  # pragma: no cover\n            return\n        # Function will always be called with a key like attributes.\n        # this is just a sanity check to ensure that is removed\n        if parts[0] == \"attributes\":\n            parts = parts[1:]\n        set_path_in_dict(user.attributes, \".\".join(parts), value)\n\n    def ensure_user(self) -> tuple[User | None, bool]:\n        \"\"\"Ensure a user exists\"\"\"\n        user_created = False\n        path = self.executor.plan.context.get(\n            PLAN_CONTEXT_USER_PATH, self.executor.current_stage.user_path_template\n        )\n        if path == \"\":\n            path = User.default_path()\n\n        try:\n            user_type = UserTypes(\n                self.executor.plan.context.get(\n                    PLAN_CONTEXT_USER_TYPE,\n                    self.executor.current_stage.user_type,\n                )\n            )\n        except ValueError:\n            user_type = self.executor.current_stage.user_type\n        if user_type == UserTypes.INTERNAL_SERVICE_ACCOUNT:\n            user_type = UserTypes.SERVICE_ACCOUNT\n\n        if not self.request.user.is_anonymous:\n            self.executor.plan.context.setdefault(PLAN_CONTEXT_PENDING_USER, self.request.user)\n        if (\n            PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context\n            or self.executor.current_stage.user_creation_mode == UserCreationMode.ALWAYS_CREATE\n        ):\n            if self.executor.current_stage.user_creation_mode == UserCreationMode.NEVER_CREATE:\n                return None, False\n            self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(\n                is_active=not self.executor.current_stage.create_users_as_inactive,\n                path=path,\n                type=user_type,\n            )\n            self.executor.plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT\n            self.logger.debug(\n                \"Created new user\",\n                flow_slug=self.executor.flow.slug,\n            )\n            user_created = True\n        user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]\n        return user, user_created\n\n    def update_user(self, user: User):\n        \"\"\"Update `user` with data from plan context\n\n        Only simple attributes are updated, nothing which requires a foreign key or m2m\"\"\"\n        data: dict = self.executor.plan.context[PLAN_CONTEXT_PROMPT]\n        # This is always sent back but not written to the user\n        data.pop(\"component\", None)\n        for key, value in data.items():\n            setter_name = f\"set_{key}\"\n            # Check if user has a setter for this key, like set_password\n            if hasattr(user, setter_name):\n                setter = getattr(user, setter_name)\n                if callable(setter):\n                    setter(value)\n            elif key in self.disallowed_user_attributes:\n                self.logger.info(\"discarding key\", key=key)\n                continue\n            # For exact attributes match, update the dictionary in place\n            elif key == \"attributes\":\n                user.attributes.update(value)\n            # If using dot notation, use the correct helper to update the nested value\n            elif key.startswith(\"attributes.\") or key.startswith(\"attributes_\"):\n                UserWriteStageView.write_attribute(user, key, value)\n            # User has this key already\n            elif hasattr(user, key):\n                setattr(user, key, value)\n            # If none of the cases above matched, we have an attribute that the user doesn't have,\n            # has no setter for, is not a nested attributes value and as such is invalid\n            else:\n                self.logger.info(\"discarding key\", key=key)\n                continue\n        # Check if we're writing from a source, and save the source to the attributes\n        if PLAN_CONTEXT_SOURCES_CONNECTION in self.executor.plan.context:\n            if USER_ATTRIBUTE_SOURCES not in user.attributes or not isinstance(\n                user.attributes.get(USER_ATTRIBUTE_SOURCES), list\n            ):\n                user.attributes[USER_ATTRIBUTE_SOURCES] = []\n            connection: UserSourceConnection = self.executor.plan.context[\n                PLAN_CONTEXT_SOURCES_CONNECTION\n            ]\n            if connection.source.name not in user.attributes[USER_ATTRIBUTE_SOURCES]:\n                user.attributes[USER_ATTRIBUTE_SOURCES].append(connection.source.name)\n\n    def dispatch(self, request: HttpRequest) -> HttpResponse:\n        \"\"\"Save data in the current flow to the currently pending user. If no user is pending,\n        a new user is created.\"\"\"\n        if PLAN_CONTEXT_PROMPT not in self.executor.plan.context:\n            message = _(\"No Pending data.\")\n            self.logger.debug(message)\n            return self.executor.stage_invalid(message)\n        data = self.executor.plan.context[PLAN_CONTEXT_PROMPT]\n        user, user_created = self.ensure_user()\n        if not user:\n            message = _(\"No user found and can't create new user.\")\n            self.logger.info(message)\n            return self.executor.stage_invalid(message)\n        # Before we change anything, check if the user is the same as in the request\n        # and we're updating a password. In that case we need to update the session hash\n        # Also check that we're not currently impersonating, so we don't update the session\n        should_update_session = False\n        if (\n            any(\"password\" in x for x in data.keys())\n            and self.request.user.pk == user.pk\n            and SESSION_KEY_IMPERSONATE_USER not in self.request.session\n        ):\n            should_update_session = True\n        try:\n            self.update_user(user)\n        except ValidationError as exc:\n            self.logger.warning(\"failed to update user\", exc=exc)\n            return self.executor.stage_invalid(_(\"Failed to update user. Please try again later.\"))\n        # Extra check to prevent flows from saving a user with a blank username\n        if user.username == \"\":\n            self.logger.warning(\"Aborting write to empty username\", user=user)\n            return self.executor.stage_invalid()\n        try:\n            with transaction.atomic():\n                user.save()\n                if self.executor.current_stage.create_users_group:\n                    user.ak_groups.add(self.executor.current_stage.create_users_group)\n                if PLAN_CONTEXT_GROUPS in self.executor.plan.context:\n                    user.ak_groups.add(*self.executor.plan.context[PLAN_CONTEXT_GROUPS])\n        except (IntegrityError, ValueError, TypeError, InternalError) as exc:\n            self.logger.warning(\"Failed to save user\", exc=exc)\n            return self.executor.stage_invalid(_(\"Failed to update user. Please try again later.\"))\n        user_write.send(sender=self, request=request, user=user, data=data, created=user_created)\n        # Check if the password has been updated, and update the session auth hash\n        if should_update_session:\n            update_session_auth_hash(self.request, user)\n            self.logger.debug(\"Updated session hash\", user=user)\n        self.logger.debug(\n            \"Updated existing user\",\n            user=user,\n            flow_slug=self.executor.flow.slug,\n        )\n        return self.executor.stage_ok()\n", "path": "authentik/stages/user_write/stage.py"}], "after_files": [{"content": "\"\"\"Write stage logic\"\"\"\n\nfrom typing import Any\n\nfrom django.contrib.auth import update_session_auth_hash\nfrom django.db import transaction\nfrom django.db.utils import IntegrityError, InternalError\nfrom django.http import HttpRequest, HttpResponse\nfrom django.utils.translation import gettext as _\nfrom rest_framework.exceptions import ValidationError\n\nfrom authentik.core.middleware import SESSION_KEY_IMPERSONATE_USER\nfrom authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection, UserTypes\nfrom authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION\nfrom authentik.events.utils import sanitize_item\nfrom authentik.flows.planner import PLAN_CONTEXT_PENDING_USER\nfrom authentik.flows.stage import StageView\nfrom authentik.flows.views.executor import FlowExecutorView\nfrom authentik.lib.config import set_path_in_dict\nfrom authentik.stages.password import BACKEND_INBUILT\nfrom authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND\nfrom authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT\nfrom authentik.stages.user_write.models import UserCreationMode\nfrom authentik.stages.user_write.signals import user_write\n\nPLAN_CONTEXT_GROUPS = \"groups\"\nPLAN_CONTEXT_USER_TYPE = \"user_type\"\nPLAN_CONTEXT_USER_PATH = \"user_path\"\n\n\nclass UserWriteStageView(StageView):\n    \"\"\"Finalise Enrollment flow by creating a user object.\"\"\"\n\n    def __init__(self, executor: FlowExecutorView, **kwargs):\n        super().__init__(executor, **kwargs)\n        self.disallowed_user_attributes = [\n            \"groups\",\n        ]\n\n    @staticmethod\n    def write_attribute(user: User, key: str, value: Any):\n        \"\"\"Allow use of attributes.foo.bar when writing to a user, with full\n        recursion\"\"\"\n        parts = key.replace(\"_\", \".\").split(\".\")\n        if len(parts) < 1:  # pragma: no cover\n            return\n        # Function will always be called with a key like attributes.\n        # this is just a sanity check to ensure that is removed\n        if parts[0] == \"attributes\":\n            parts = parts[1:]\n        set_path_in_dict(user.attributes, \".\".join(parts), sanitize_item(value))\n\n    def ensure_user(self) -> tuple[User | None, bool]:\n        \"\"\"Ensure a user exists\"\"\"\n        user_created = False\n        path = self.executor.plan.context.get(\n            PLAN_CONTEXT_USER_PATH, self.executor.current_stage.user_path_template\n        )\n        if path == \"\":\n            path = User.default_path()\n\n        try:\n            user_type = UserTypes(\n                self.executor.plan.context.get(\n                    PLAN_CONTEXT_USER_TYPE,\n                    self.executor.current_stage.user_type,\n                )\n            )\n        except ValueError:\n            user_type = self.executor.current_stage.user_type\n        if user_type == UserTypes.INTERNAL_SERVICE_ACCOUNT:\n            user_type = UserTypes.SERVICE_ACCOUNT\n\n        if not self.request.user.is_anonymous:\n            self.executor.plan.context.setdefault(PLAN_CONTEXT_PENDING_USER, self.request.user)\n        if (\n            PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context\n            or self.executor.current_stage.user_creation_mode == UserCreationMode.ALWAYS_CREATE\n        ):\n            if self.executor.current_stage.user_creation_mode == UserCreationMode.NEVER_CREATE:\n                return None, False\n            self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(\n                is_active=not self.executor.current_stage.create_users_as_inactive,\n                path=path,\n                type=user_type,\n            )\n            self.executor.plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT\n            self.logger.debug(\n                \"Created new user\",\n                flow_slug=self.executor.flow.slug,\n            )\n            user_created = True\n        user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]\n        return user, user_created\n\n    def update_user(self, user: User):\n        \"\"\"Update `user` with data from plan context\n\n        Only simple attributes are updated, nothing which requires a foreign key or m2m\"\"\"\n        data: dict = self.executor.plan.context[PLAN_CONTEXT_PROMPT]\n        # This is always sent back but not written to the user\n        data.pop(\"component\", None)\n        for key, value in data.items():\n            setter_name = f\"set_{key}\"\n            # Check if user has a setter for this key, like set_password\n            if hasattr(user, setter_name):\n                setter = getattr(user, setter_name)\n                if callable(setter):\n                    setter(value)\n            elif key in self.disallowed_user_attributes:\n                self.logger.info(\"discarding key\", key=key)\n                continue\n            # For exact attributes match, update the dictionary in place\n            elif key == \"attributes\":\n                user.attributes.update(value)\n            # If using dot notation, use the correct helper to update the nested value\n            elif key.startswith(\"attributes.\") or key.startswith(\"attributes_\"):\n                UserWriteStageView.write_attribute(user, key, value)\n            # User has this key already\n            elif hasattr(user, key):\n                setattr(user, key, value)\n            # If none of the cases above matched, we have an attribute that the user doesn't have,\n            # has no setter for, is not a nested attributes value and as such is invalid\n            else:\n                self.logger.info(\"discarding key\", key=key)\n                continue\n        # Check if we're writing from a source, and save the source to the attributes\n        if PLAN_CONTEXT_SOURCES_CONNECTION in self.executor.plan.context:\n            if USER_ATTRIBUTE_SOURCES not in user.attributes or not isinstance(\n                user.attributes.get(USER_ATTRIBUTE_SOURCES), list\n            ):\n                user.attributes[USER_ATTRIBUTE_SOURCES] = []\n            connection: UserSourceConnection = self.executor.plan.context[\n                PLAN_CONTEXT_SOURCES_CONNECTION\n            ]\n            if connection.source.name not in user.attributes[USER_ATTRIBUTE_SOURCES]:\n                user.attributes[USER_ATTRIBUTE_SOURCES].append(connection.source.name)\n\n    def dispatch(self, request: HttpRequest) -> HttpResponse:\n        \"\"\"Save data in the current flow to the currently pending user. If no user is pending,\n        a new user is created.\"\"\"\n        if PLAN_CONTEXT_PROMPT not in self.executor.plan.context:\n            message = _(\"No Pending data.\")\n            self.logger.debug(message)\n            return self.executor.stage_invalid(message)\n        data = self.executor.plan.context[PLAN_CONTEXT_PROMPT]\n        user, user_created = self.ensure_user()\n        if not user:\n            message = _(\"No user found and can't create new user.\")\n            self.logger.info(message)\n            return self.executor.stage_invalid(message)\n        # Before we change anything, check if the user is the same as in the request\n        # and we're updating a password. In that case we need to update the session hash\n        # Also check that we're not currently impersonating, so we don't update the session\n        should_update_session = False\n        if (\n            any(\"password\" in x for x in data.keys())\n            and self.request.user.pk == user.pk\n            and SESSION_KEY_IMPERSONATE_USER not in self.request.session\n        ):\n            should_update_session = True\n        try:\n            self.update_user(user)\n        except ValidationError as exc:\n            self.logger.warning(\"failed to update user\", exc=exc)\n            return self.executor.stage_invalid(_(\"Failed to update user. Please try again later.\"))\n        # Extra check to prevent flows from saving a user with a blank username\n        if user.username == \"\":\n            self.logger.warning(\"Aborting write to empty username\", user=user)\n            return self.executor.stage_invalid()\n        try:\n            with transaction.atomic():\n                user.save()\n                if self.executor.current_stage.create_users_group:\n                    user.ak_groups.add(self.executor.current_stage.create_users_group)\n                if PLAN_CONTEXT_GROUPS in self.executor.plan.context:\n                    user.ak_groups.add(*self.executor.plan.context[PLAN_CONTEXT_GROUPS])\n        except (IntegrityError, ValueError, TypeError, InternalError) as exc:\n            self.logger.warning(\"Failed to save user\", exc=exc)\n            return self.executor.stage_invalid(_(\"Failed to update user. Please try again later.\"))\n        user_write.send(sender=self, request=request, user=user, data=data, created=user_created)\n        # Check if the password has been updated, and update the session auth hash\n        if should_update_session:\n            update_session_auth_hash(self.request, user)\n            self.logger.debug(\"Updated session hash\", user=user)\n        self.logger.debug(\n            \"Updated existing user\",\n            user=user,\n            flow_slug=self.executor.flow.slug,\n        )\n        return self.executor.stage_ok()\n", "path": "authentik/stages/user_write/stage.py"}]}