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'
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.
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
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
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.