authentik.stages.user_write.stage

Write stage logic

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

Finalise Enrollment flow by creating a user object.

UserWriteStageView(executor: authentik.flows.views.executor.FlowExecutorView, **kwargs)
36    def __init__(self, executor: FlowExecutorView, **kwargs):
37        super().__init__(executor, **kwargs)
38        self.disallowed_user_attributes = [
39            "groups",
40        ]

Constructor. Called in the URLconf; can contain helpful extra keyword arguments, and other things.

disallowed_user_attributes
@staticmethod
def write_attribute(user: authentik.core.models.User, key: str, value: Any):
42    @staticmethod
43    def write_attribute(user: User, key: str, value: Any):
44        """Allow use of attributes.foo.bar when writing to a user, with full
45        recursion"""
46        parts = key.replace("attributes_", "attributes.", 1).split(".")
47        if len(parts) < 1:  # pragma: no cover
48            return
49        # Function will always be called with a key like attributes.
50        # this is just a sanity check to ensure that is removed
51        if parts[0] == "attributes":
52            parts = parts[1:]
53        set_path_in_dict(user.attributes, ".".join(parts), sanitize_item(value))

Allow use of attributes.foo.bar when writing to a user, with full recursion

def ensure_user(self) -> tuple[authentik.core.models.User | None, bool]:
55    def ensure_user(self) -> tuple[User | None, bool]:
56        """Ensure a user exists"""
57        user_created = False
58        path = self.executor.plan.context.get(
59            PLAN_CONTEXT_USER_PATH, self.executor.current_stage.user_path_template
60        )
61        if path == "":
62            path = User.default_path()
63
64        try:
65            user_type = UserTypes(
66                self.executor.plan.context.get(
67                    PLAN_CONTEXT_USER_TYPE,
68                    self.executor.current_stage.user_type,
69                )
70            )
71        except ValueError:
72            user_type = self.executor.current_stage.user_type
73        if user_type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
74            user_type = UserTypes.SERVICE_ACCOUNT
75
76        if not self.request.user.is_anonymous:
77            self.executor.plan.context.setdefault(PLAN_CONTEXT_PENDING_USER, self.request.user)
78        if (
79            PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context
80            or self.executor.current_stage.user_creation_mode == UserCreationMode.ALWAYS_CREATE
81        ):
82            if self.executor.current_stage.user_creation_mode == UserCreationMode.NEVER_CREATE:
83                return None, False
84            self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(
85                is_active=not self.executor.current_stage.create_users_as_inactive,
86                path=path,
87                type=user_type,
88            )
89            self.executor.plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT
90            self.logger.debug(
91                "Created new user",
92                flow_slug=self.executor.flow.slug,
93            )
94            user_created = True
95        user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
96        return user, user_created

Ensure a user exists

def update_user(self, user: authentik.core.models.User):
 98    def update_user(self, user: User):
 99        """Update `user` with data from plan context
100
101        Only simple attributes are updated, nothing which requires a foreign key or m2m"""
102        data: dict = self.executor.plan.context[PLAN_CONTEXT_PROMPT]
103        # This is always sent back but not written to the user
104        data.pop("component", None)
105        for key, value in data.items():
106            setter_name = f"set_{key}"
107            # Check if user has a setter for this key, like set_password
108            if key == "password":
109                user.set_password(value, request=self.request)
110            elif hasattr(user, setter_name):
111                setter = getattr(user, setter_name)
112                if callable(setter):
113                    setter(value)
114            elif key in self.disallowed_user_attributes:
115                self.logger.info("discarding key", key=key)
116                continue
117            # For exact attributes match, update the dictionary in place
118            elif key == "attributes":
119                if isinstance(value, dict):
120                    user.attributes.update(sanitize_dict(value))
121                else:
122                    raise ValidationError("Attempt to overwrite complete attributes")
123            # If using dot notation, use the correct helper to update the nested value
124            elif key.startswith("attributes.") or key.startswith("attributes_"):
125                UserWriteStageView.write_attribute(user, key, value)
126            # User has this key already
127            elif hasattr(user, key):
128                if isinstance(user, SimpleLazyObject):
129                    user._setup()
130                    user = user._wrapped
131                attr = getattr(type(user), key)
132                if isinstance(attr, property):
133                    if not attr.fset:
134                        self.logger.info("discarding key", key=key)
135                        continue
136                setattr(user, key, value)
137            # If none of the cases above matched, we have an attribute that the user doesn't have,
138            # has no setter for, is not a nested attributes value and as such is invalid
139            else:
140                self.logger.info("discarding key", key=key)
141                continue
142        # Check if we're writing from a source, and save the source to the attributes
143        if PLAN_CONTEXT_SOURCES_CONNECTION in self.executor.plan.context:
144            if USER_ATTRIBUTE_SOURCES not in user.attributes or not isinstance(
145                user.attributes.get(USER_ATTRIBUTE_SOURCES), list
146            ):
147                user.attributes[USER_ATTRIBUTE_SOURCES] = []
148            connection: UserSourceConnection = self.executor.plan.context[
149                PLAN_CONTEXT_SOURCES_CONNECTION
150            ]
151            if connection.source.name not in user.attributes[USER_ATTRIBUTE_SOURCES]:
152                user.attributes[USER_ATTRIBUTE_SOURCES].append(connection.source.name)

Update user with data from plan context

Only simple attributes are updated, nothing which requires a foreign key or m2m

def dispatch( self, request: django.http.request.HttpRequest) -> django.http.response.HttpResponse:
154    def dispatch(self, request: HttpRequest) -> HttpResponse:
155        """Save data in the current flow to the currently pending user. If no user is pending,
156        a new user is created."""
157        if PLAN_CONTEXT_PROMPT not in self.executor.plan.context:
158            message = _("No Pending data.")
159            self.logger.debug(message)
160            return self.executor.stage_invalid(message)
161        data = self.executor.plan.context[PLAN_CONTEXT_PROMPT]
162        user, user_created = self.ensure_user()
163        if not user:
164            message = _("No user found and can't create new user.")
165            self.logger.info(message)
166            return self.executor.stage_invalid(message)
167        # Before we change anything, check if the user is the same as in the request
168        # and we're updating a password. In that case we need to update the session hash
169        # Also check that we're not currently impersonating, so we don't update the session
170        should_update_session = False
171        if (
172            any("password" in x for x in data.keys())
173            and self.request.user.pk == user.pk
174            and SESSION_KEY_IMPERSONATE_USER not in self.request.session
175        ):
176            should_update_session = True
177        try:
178            self.update_user(user)
179        except ValidationError as exc:
180            self.logger.warning("failed to update user", exc=exc)
181            return self.executor.stage_invalid(_("Failed to update user. Please try again later."))
182        # Extra check to prevent flows from saving a user with a blank username
183        if user.username == "":
184            self.logger.warning("Aborting write to empty username", user=user)
185            return self.executor.stage_invalid()
186        try:
187            with transaction.atomic():
188                user.save()
189                if self.executor.current_stage.create_users_group:
190                    user.groups.add(self.executor.current_stage.create_users_group)
191                if PLAN_CONTEXT_GROUPS in self.executor.plan.context:
192                    user.groups.add(*self.executor.plan.context[PLAN_CONTEXT_GROUPS])
193        except (IntegrityError, ValueError, TypeError, InternalError) as exc:
194            self.logger.warning("Failed to save user", exc=exc)
195            return self.executor.stage_invalid(_("Failed to update user. Please try again later."))
196        user_write.send(sender=self, request=request, user=user, data=data, created=user_created)
197        # Check if the password has been updated, and update the session auth hash
198        if should_update_session:
199            update_session_auth_hash(self.request, user)
200            self.logger.debug("Updated session hash", user=user)
201        self.logger.debug(
202            "Updated existing user",
203            user=user,
204            flow_slug=self.executor.flow.slug,
205        )
206        return self.executor.stage_ok()

Save data in the current flow to the currently pending user. If no user is pending, a new user is created.