authentik.stages.prompt.models
prompt models
1"""prompt models""" 2 3from typing import Any, Type # noqa: UP035 4from urllib.parse import urlparse, urlunparse 5from uuid import uuid4 6 7from django.db import models 8from django.http import HttpRequest 9from django.utils.translation import gettext_lazy as _ 10from django.views import View 11from rest_framework.exceptions import ValidationError 12from rest_framework.fields import ( 13 BooleanField, 14 CharField, 15 ChoiceField, 16 DateField, 17 DateTimeField, 18 EmailField, 19 HiddenField, 20 IntegerField, 21 ReadOnlyField, 22) 23from rest_framework.serializers import BaseSerializer 24from structlog.stdlib import get_logger 25 26from authentik.core.expression.evaluator import PropertyMappingEvaluator 27from authentik.core.expression.exceptions import PropertyMappingExpressionException 28from authentik.core.models import User 29from authentik.flows.models import Stage 30from authentik.lib.models import SerializerModel 31from authentik.policies.models import Policy 32 33CHOICES_CONTEXT_SUFFIX = "__choices" 34 35LOGGER = get_logger() 36 37 38class FieldTypes(models.TextChoices): 39 """Field types an Prompt can be""" 40 41 # update website/docs/add-secure-apps/flows-stages/stages/prompt/index.md 42 43 # Simple text field 44 TEXT = "text", _("Text: Simple Text input") 45 # Long text field 46 TEXT_AREA = "text_area", _("Text area: Multiline Text Input.") 47 # Simple text field 48 TEXT_READ_ONLY = "text_read_only", _( 49 "Text (read-only): Simple Text input, but cannot be edited." 50 ) 51 # Long text field 52 TEXT_AREA_READ_ONLY = "text_area_read_only", _( 53 "Text area (read-only): Multiline Text input, but cannot be edited." 54 ) 55 56 # Same as text, but has autocomplete for password managers 57 USERNAME = ( 58 "username", 59 _("Username: Same as Text input, but checks for and prevents duplicate usernames."), 60 ) 61 EMAIL = "email", _("Email: Text field with Email type.") 62 PASSWORD = ( 63 "password", # noqa # nosec 64 _( 65 "Password: Masked input, multiple inputs of this type on the same prompt " 66 "need to be identical." 67 ), 68 ) 69 NUMBER = "number" 70 CHECKBOX = "checkbox" 71 RADIO_BUTTON_GROUP = "radio-button-group", _( 72 "Fixed choice field rendered as a group of radio buttons." 73 ) 74 DROPDOWN = "dropdown", _("Fixed choice field rendered as a dropdown.") 75 DATE = "date" 76 DATE_TIME = "date-time" 77 78 FILE = ( 79 "file", 80 _( 81 "File: File upload for arbitrary files. File content will be available in flow " 82 "context as data-URI" 83 ), 84 ) 85 86 SEPARATOR = "separator", _("Separator: Static Separator Line") 87 HIDDEN = "hidden", _("Hidden: Hidden field, can be used to insert data into form.") 88 STATIC = "static", _("Static: Static value, displayed as-is.") 89 90 AK_LOCALE = "ak-locale", _("authentik: Selection of locales authentik supports") 91 92 93CHOICE_FIELDS = (FieldTypes.RADIO_BUTTON_GROUP, FieldTypes.DROPDOWN) 94 95 96class InlineFileField(CharField): 97 """Field for inline data-URI base64 encoded files""" 98 99 def to_internal_value(self, data: str): 100 uri = urlparse(data) 101 if uri.scheme != "data": 102 raise ValidationError("Invalid scheme") 103 header, _encoded = uri.path.split(",", 1) 104 _mime, _, enc = header.partition(";") 105 if enc != "base64": 106 raise ValidationError("Invalid encoding") 107 return super().to_internal_value(urlunparse(uri)) 108 109 110class Prompt(SerializerModel): 111 """Single Prompt, part of a prompt stage.""" 112 113 prompt_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) 114 name = models.TextField(unique=True, blank=False) 115 116 field_key = models.TextField( 117 help_text=_("Name of the form field, also used to store the value") 118 ) 119 label = models.TextField() 120 type = models.CharField(max_length=100, choices=FieldTypes.choices) 121 required = models.BooleanField(default=True) 122 placeholder = models.TextField( 123 blank=True, 124 help_text=_( 125 "Optionally provide a short hint that describes the expected input value. " 126 "When creating a fixed choice field, enable interpreting as " 127 "expression and return a list to return multiple choices." 128 ), 129 ) 130 initial_value = models.TextField( 131 blank=True, 132 help_text=_( 133 "Optionally pre-fill the input with an initial value. " 134 "When creating a fixed choice field, enable interpreting as " 135 "expression and return a list to return multiple default choices." 136 ), 137 ) 138 sub_text = models.TextField(blank=True, default="") 139 140 order = models.IntegerField(default=0) 141 142 placeholder_expression = models.BooleanField(default=False) 143 initial_value_expression = models.BooleanField(default=False) 144 145 @property 146 def serializer(self) -> Type[BaseSerializer]: # noqa: UP006 147 from authentik.stages.prompt.api import PromptSerializer 148 149 return PromptSerializer 150 151 def get_choices( 152 self, 153 prompt_context: dict, 154 user: User, 155 request: HttpRequest, 156 dry_run: bool | None = False, 157 ) -> tuple[dict[str, Any]] | None: 158 """Get fully interpolated list of choices""" 159 if self.type not in CHOICE_FIELDS: 160 return None 161 162 raw_choices = self.placeholder 163 164 if self.field_key + CHOICES_CONTEXT_SUFFIX in prompt_context: 165 raw_choices = prompt_context[self.field_key + CHOICES_CONTEXT_SUFFIX] 166 elif self.placeholder_expression: 167 evaluator = PropertyMappingEvaluator( 168 self, user, request, prompt_context=prompt_context, dry_run=dry_run 169 ) 170 try: 171 raw_choices = evaluator.evaluate(self.placeholder) 172 except Exception as exc: # pylint:disable=broad-except 173 wrapped = PropertyMappingExpressionException(exc, None) 174 LOGGER.warning( 175 "failed to evaluate prompt choices", 176 exc=wrapped, 177 ) 178 if dry_run: 179 raise wrapped from exc 180 181 if isinstance(raw_choices, list | tuple | set): 182 choices = raw_choices 183 else: 184 choices = [raw_choices] 185 186 if len(choices) == 0: 187 LOGGER.warning("failed to get prompt choices", choices=choices, input=raw_choices) 188 189 return tuple(choices) 190 191 def get_placeholder( 192 self, 193 prompt_context: dict, 194 user: User, 195 request: HttpRequest, 196 dry_run: bool | None = False, 197 ) -> str: 198 """Get fully interpolated placeholder""" 199 if self.type in CHOICE_FIELDS: 200 # Choice fields use the placeholder to define all valid choices. 201 # Therefore their actual placeholder is always blank 202 return "" 203 204 if self.placeholder_expression: 205 evaluator = PropertyMappingEvaluator( 206 self, user, request, prompt_context=prompt_context, dry_run=dry_run 207 ) 208 try: 209 return evaluator.evaluate(self.placeholder) 210 except Exception as exc: # pylint:disable=broad-except 211 wrapped = PropertyMappingExpressionException(exc, None) 212 LOGGER.warning( 213 "failed to evaluate prompt placeholder", 214 exc=wrapped, 215 ) 216 if dry_run: 217 raise wrapped from exc 218 return self.placeholder 219 220 def get_initial_value( 221 self, 222 prompt_context: dict, 223 user: User, 224 request: HttpRequest, 225 dry_run: bool | None = False, 226 ) -> str: 227 """Get fully interpolated initial value""" 228 229 if self.field_key in prompt_context: 230 # We don't want to parse this as an expression since a user will 231 # be able to control the input 232 value = prompt_context[self.field_key] 233 elif self.initial_value_expression: 234 evaluator = PropertyMappingEvaluator( 235 self, user, request, prompt_context=prompt_context, dry_run=dry_run 236 ) 237 try: 238 value = evaluator.evaluate(self.initial_value) 239 except Exception as exc: # pylint:disable=broad-except 240 wrapped = PropertyMappingExpressionException(exc, None) 241 LOGGER.warning( 242 "failed to evaluate prompt initial value", 243 exc=wrapped, 244 ) 245 if dry_run: 246 raise wrapped from exc 247 value = self.initial_value 248 else: 249 value = self.initial_value 250 251 if self.type in CHOICE_FIELDS: 252 # Ensure returned value is a valid choice 253 choices = self.get_choices(prompt_context, user, request) 254 if not choices: 255 return "" 256 if not any( 257 choice.get("value") == value if isinstance(choice, dict) else choice == value 258 for choice in choices 259 ): 260 return choices[0] 261 262 return value 263 264 def field( # noqa PLR0915 265 self, default: Any | None, choices: list[Any] | None = None 266 ) -> CharField: 267 """Get field type for Challenge and response. Choices are only valid for CHOICE_FIELDS.""" 268 field_class = CharField 269 kwargs = { 270 "required": self.required, 271 } 272 match self.type: 273 case FieldTypes.TEXT | FieldTypes.TEXT_AREA: 274 kwargs["trim_whitespace"] = False 275 kwargs["allow_blank"] = not self.required 276 case FieldTypes.TEXT_READ_ONLY, FieldTypes.TEXT_AREA_READ_ONLY: 277 field_class = ReadOnlyField 278 # required can't be set for ReadOnlyField 279 kwargs["required"] = False 280 kwargs["allow_blank"] = True 281 case FieldTypes.EMAIL: 282 field_class = EmailField 283 kwargs["allow_blank"] = not self.required 284 case FieldTypes.NUMBER: 285 field_class = IntegerField 286 case FieldTypes.CHECKBOX: 287 field_class = BooleanField 288 kwargs["required"] = False 289 case FieldTypes.DATE: 290 field_class = DateField 291 case FieldTypes.DATE_TIME: 292 field_class = DateTimeField 293 case FieldTypes.FILE: 294 field_class = InlineFileField 295 case FieldTypes.SEPARATOR: 296 kwargs["required"] = False 297 kwargs["label"] = "" 298 case FieldTypes.HIDDEN: 299 field_class = HiddenField 300 kwargs["required"] = False 301 kwargs["default"] = self.placeholder 302 case FieldTypes.STATIC: 303 kwargs["default"] = self.placeholder 304 kwargs["required"] = False 305 kwargs["label"] = "" 306 307 case FieldTypes.AK_LOCALE: 308 kwargs["allow_blank"] = True 309 310 if self.type in CHOICE_FIELDS: 311 field_class = ChoiceField 312 kwargs["choices"] = [] 313 if choices: 314 for choice in choices: 315 label, value = choice, choice 316 if isinstance(choice, dict): 317 label = choice.get("label", "") 318 value = choice.get("value", "") 319 kwargs["choices"].append((value, label)) 320 321 if default: 322 kwargs["default"] = default 323 # May not set both `required` and `default` 324 if "default" in kwargs: 325 kwargs.pop("required", None) 326 return field_class(**kwargs) 327 328 def save(self, *args, **kwargs): 329 if self.type not in FieldTypes: 330 raise ValueError 331 return super().save(*args, **kwargs) 332 333 def __str__(self): 334 return f"Prompt field '{self.field_key}' type {self.type}" 335 336 class Meta: 337 verbose_name = _("Prompt") 338 verbose_name_plural = _("Prompts") 339 340 341class PromptStage(Stage): 342 """Prompt the user to enter information.""" 343 344 fields = models.ManyToManyField(Prompt) 345 346 validation_policies = models.ManyToManyField(Policy, blank=True) 347 348 @property 349 def serializer(self) -> type[BaseSerializer]: 350 from authentik.stages.prompt.api import PromptStageSerializer 351 352 return PromptStageSerializer 353 354 @property 355 def view(self) -> type[View]: 356 from authentik.stages.prompt.stage import PromptStageView 357 358 return PromptStageView 359 360 @property 361 def component(self) -> str: 362 return "ak-stage-prompt-form" 363 364 class Meta: 365 verbose_name = _("Prompt Stage") 366 verbose_name_plural = _("Prompt Stages")
39class FieldTypes(models.TextChoices): 40 """Field types an Prompt can be""" 41 42 # update website/docs/add-secure-apps/flows-stages/stages/prompt/index.md 43 44 # Simple text field 45 TEXT = "text", _("Text: Simple Text input") 46 # Long text field 47 TEXT_AREA = "text_area", _("Text area: Multiline Text Input.") 48 # Simple text field 49 TEXT_READ_ONLY = "text_read_only", _( 50 "Text (read-only): Simple Text input, but cannot be edited." 51 ) 52 # Long text field 53 TEXT_AREA_READ_ONLY = "text_area_read_only", _( 54 "Text area (read-only): Multiline Text input, but cannot be edited." 55 ) 56 57 # Same as text, but has autocomplete for password managers 58 USERNAME = ( 59 "username", 60 _("Username: Same as Text input, but checks for and prevents duplicate usernames."), 61 ) 62 EMAIL = "email", _("Email: Text field with Email type.") 63 PASSWORD = ( 64 "password", # noqa # nosec 65 _( 66 "Password: Masked input, multiple inputs of this type on the same prompt " 67 "need to be identical." 68 ), 69 ) 70 NUMBER = "number" 71 CHECKBOX = "checkbox" 72 RADIO_BUTTON_GROUP = "radio-button-group", _( 73 "Fixed choice field rendered as a group of radio buttons." 74 ) 75 DROPDOWN = "dropdown", _("Fixed choice field rendered as a dropdown.") 76 DATE = "date" 77 DATE_TIME = "date-time" 78 79 FILE = ( 80 "file", 81 _( 82 "File: File upload for arbitrary files. File content will be available in flow " 83 "context as data-URI" 84 ), 85 ) 86 87 SEPARATOR = "separator", _("Separator: Static Separator Line") 88 HIDDEN = "hidden", _("Hidden: Hidden field, can be used to insert data into form.") 89 STATIC = "static", _("Static: Static value, displayed as-is.") 90 91 AK_LOCALE = "ak-locale", _("authentik: Selection of locales authentik supports")
Field types an Prompt can be
97class InlineFileField(CharField): 98 """Field for inline data-URI base64 encoded files""" 99 100 def to_internal_value(self, data: str): 101 uri = urlparse(data) 102 if uri.scheme != "data": 103 raise ValidationError("Invalid scheme") 104 header, _encoded = uri.path.split(",", 1) 105 _mime, _, enc = header.partition(";") 106 if enc != "base64": 107 raise ValidationError("Invalid encoding") 108 return super().to_internal_value(urlunparse(uri))
Field for inline data-URI base64 encoded files
100 def to_internal_value(self, data: str): 101 uri = urlparse(data) 102 if uri.scheme != "data": 103 raise ValidationError("Invalid scheme") 104 header, _encoded = uri.path.split(",", 1) 105 _mime, _, enc = header.partition(";") 106 if enc != "base64": 107 raise ValidationError("Invalid encoding") 108 return super().to_internal_value(urlunparse(uri))
Transform the incoming primitive data into a native value.
111class Prompt(SerializerModel): 112 """Single Prompt, part of a prompt stage.""" 113 114 prompt_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) 115 name = models.TextField(unique=True, blank=False) 116 117 field_key = models.TextField( 118 help_text=_("Name of the form field, also used to store the value") 119 ) 120 label = models.TextField() 121 type = models.CharField(max_length=100, choices=FieldTypes.choices) 122 required = models.BooleanField(default=True) 123 placeholder = models.TextField( 124 blank=True, 125 help_text=_( 126 "Optionally provide a short hint that describes the expected input value. " 127 "When creating a fixed choice field, enable interpreting as " 128 "expression and return a list to return multiple choices." 129 ), 130 ) 131 initial_value = models.TextField( 132 blank=True, 133 help_text=_( 134 "Optionally pre-fill the input with an initial value. " 135 "When creating a fixed choice field, enable interpreting as " 136 "expression and return a list to return multiple default choices." 137 ), 138 ) 139 sub_text = models.TextField(blank=True, default="") 140 141 order = models.IntegerField(default=0) 142 143 placeholder_expression = models.BooleanField(default=False) 144 initial_value_expression = models.BooleanField(default=False) 145 146 @property 147 def serializer(self) -> Type[BaseSerializer]: # noqa: UP006 148 from authentik.stages.prompt.api import PromptSerializer 149 150 return PromptSerializer 151 152 def get_choices( 153 self, 154 prompt_context: dict, 155 user: User, 156 request: HttpRequest, 157 dry_run: bool | None = False, 158 ) -> tuple[dict[str, Any]] | None: 159 """Get fully interpolated list of choices""" 160 if self.type not in CHOICE_FIELDS: 161 return None 162 163 raw_choices = self.placeholder 164 165 if self.field_key + CHOICES_CONTEXT_SUFFIX in prompt_context: 166 raw_choices = prompt_context[self.field_key + CHOICES_CONTEXT_SUFFIX] 167 elif self.placeholder_expression: 168 evaluator = PropertyMappingEvaluator( 169 self, user, request, prompt_context=prompt_context, dry_run=dry_run 170 ) 171 try: 172 raw_choices = evaluator.evaluate(self.placeholder) 173 except Exception as exc: # pylint:disable=broad-except 174 wrapped = PropertyMappingExpressionException(exc, None) 175 LOGGER.warning( 176 "failed to evaluate prompt choices", 177 exc=wrapped, 178 ) 179 if dry_run: 180 raise wrapped from exc 181 182 if isinstance(raw_choices, list | tuple | set): 183 choices = raw_choices 184 else: 185 choices = [raw_choices] 186 187 if len(choices) == 0: 188 LOGGER.warning("failed to get prompt choices", choices=choices, input=raw_choices) 189 190 return tuple(choices) 191 192 def get_placeholder( 193 self, 194 prompt_context: dict, 195 user: User, 196 request: HttpRequest, 197 dry_run: bool | None = False, 198 ) -> str: 199 """Get fully interpolated placeholder""" 200 if self.type in CHOICE_FIELDS: 201 # Choice fields use the placeholder to define all valid choices. 202 # Therefore their actual placeholder is always blank 203 return "" 204 205 if self.placeholder_expression: 206 evaluator = PropertyMappingEvaluator( 207 self, user, request, prompt_context=prompt_context, dry_run=dry_run 208 ) 209 try: 210 return evaluator.evaluate(self.placeholder) 211 except Exception as exc: # pylint:disable=broad-except 212 wrapped = PropertyMappingExpressionException(exc, None) 213 LOGGER.warning( 214 "failed to evaluate prompt placeholder", 215 exc=wrapped, 216 ) 217 if dry_run: 218 raise wrapped from exc 219 return self.placeholder 220 221 def get_initial_value( 222 self, 223 prompt_context: dict, 224 user: User, 225 request: HttpRequest, 226 dry_run: bool | None = False, 227 ) -> str: 228 """Get fully interpolated initial value""" 229 230 if self.field_key in prompt_context: 231 # We don't want to parse this as an expression since a user will 232 # be able to control the input 233 value = prompt_context[self.field_key] 234 elif self.initial_value_expression: 235 evaluator = PropertyMappingEvaluator( 236 self, user, request, prompt_context=prompt_context, dry_run=dry_run 237 ) 238 try: 239 value = evaluator.evaluate(self.initial_value) 240 except Exception as exc: # pylint:disable=broad-except 241 wrapped = PropertyMappingExpressionException(exc, None) 242 LOGGER.warning( 243 "failed to evaluate prompt initial value", 244 exc=wrapped, 245 ) 246 if dry_run: 247 raise wrapped from exc 248 value = self.initial_value 249 else: 250 value = self.initial_value 251 252 if self.type in CHOICE_FIELDS: 253 # Ensure returned value is a valid choice 254 choices = self.get_choices(prompt_context, user, request) 255 if not choices: 256 return "" 257 if not any( 258 choice.get("value") == value if isinstance(choice, dict) else choice == value 259 for choice in choices 260 ): 261 return choices[0] 262 263 return value 264 265 def field( # noqa PLR0915 266 self, default: Any | None, choices: list[Any] | None = None 267 ) -> CharField: 268 """Get field type for Challenge and response. Choices are only valid for CHOICE_FIELDS.""" 269 field_class = CharField 270 kwargs = { 271 "required": self.required, 272 } 273 match self.type: 274 case FieldTypes.TEXT | FieldTypes.TEXT_AREA: 275 kwargs["trim_whitespace"] = False 276 kwargs["allow_blank"] = not self.required 277 case FieldTypes.TEXT_READ_ONLY, FieldTypes.TEXT_AREA_READ_ONLY: 278 field_class = ReadOnlyField 279 # required can't be set for ReadOnlyField 280 kwargs["required"] = False 281 kwargs["allow_blank"] = True 282 case FieldTypes.EMAIL: 283 field_class = EmailField 284 kwargs["allow_blank"] = not self.required 285 case FieldTypes.NUMBER: 286 field_class = IntegerField 287 case FieldTypes.CHECKBOX: 288 field_class = BooleanField 289 kwargs["required"] = False 290 case FieldTypes.DATE: 291 field_class = DateField 292 case FieldTypes.DATE_TIME: 293 field_class = DateTimeField 294 case FieldTypes.FILE: 295 field_class = InlineFileField 296 case FieldTypes.SEPARATOR: 297 kwargs["required"] = False 298 kwargs["label"] = "" 299 case FieldTypes.HIDDEN: 300 field_class = HiddenField 301 kwargs["required"] = False 302 kwargs["default"] = self.placeholder 303 case FieldTypes.STATIC: 304 kwargs["default"] = self.placeholder 305 kwargs["required"] = False 306 kwargs["label"] = "" 307 308 case FieldTypes.AK_LOCALE: 309 kwargs["allow_blank"] = True 310 311 if self.type in CHOICE_FIELDS: 312 field_class = ChoiceField 313 kwargs["choices"] = [] 314 if choices: 315 for choice in choices: 316 label, value = choice, choice 317 if isinstance(choice, dict): 318 label = choice.get("label", "") 319 value = choice.get("value", "") 320 kwargs["choices"].append((value, label)) 321 322 if default: 323 kwargs["default"] = default 324 # May not set both `required` and `default` 325 if "default" in kwargs: 326 kwargs.pop("required", None) 327 return field_class(**kwargs) 328 329 def save(self, *args, **kwargs): 330 if self.type not in FieldTypes: 331 raise ValueError 332 return super().save(*args, **kwargs) 333 334 def __str__(self): 335 return f"Prompt field '{self.field_key}' type {self.type}" 336 337 class Meta: 338 verbose_name = _("Prompt") 339 verbose_name_plural = _("Prompts")
Single Prompt, part of a prompt stage.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
146 @property 147 def serializer(self) -> Type[BaseSerializer]: # noqa: UP006 148 from authentik.stages.prompt.api import PromptSerializer 149 150 return PromptSerializer
Get serializer for this model
152 def get_choices( 153 self, 154 prompt_context: dict, 155 user: User, 156 request: HttpRequest, 157 dry_run: bool | None = False, 158 ) -> tuple[dict[str, Any]] | None: 159 """Get fully interpolated list of choices""" 160 if self.type not in CHOICE_FIELDS: 161 return None 162 163 raw_choices = self.placeholder 164 165 if self.field_key + CHOICES_CONTEXT_SUFFIX in prompt_context: 166 raw_choices = prompt_context[self.field_key + CHOICES_CONTEXT_SUFFIX] 167 elif self.placeholder_expression: 168 evaluator = PropertyMappingEvaluator( 169 self, user, request, prompt_context=prompt_context, dry_run=dry_run 170 ) 171 try: 172 raw_choices = evaluator.evaluate(self.placeholder) 173 except Exception as exc: # pylint:disable=broad-except 174 wrapped = PropertyMappingExpressionException(exc, None) 175 LOGGER.warning( 176 "failed to evaluate prompt choices", 177 exc=wrapped, 178 ) 179 if dry_run: 180 raise wrapped from exc 181 182 if isinstance(raw_choices, list | tuple | set): 183 choices = raw_choices 184 else: 185 choices = [raw_choices] 186 187 if len(choices) == 0: 188 LOGGER.warning("failed to get prompt choices", choices=choices, input=raw_choices) 189 190 return tuple(choices)
Get fully interpolated list of choices
192 def get_placeholder( 193 self, 194 prompt_context: dict, 195 user: User, 196 request: HttpRequest, 197 dry_run: bool | None = False, 198 ) -> str: 199 """Get fully interpolated placeholder""" 200 if self.type in CHOICE_FIELDS: 201 # Choice fields use the placeholder to define all valid choices. 202 # Therefore their actual placeholder is always blank 203 return "" 204 205 if self.placeholder_expression: 206 evaluator = PropertyMappingEvaluator( 207 self, user, request, prompt_context=prompt_context, dry_run=dry_run 208 ) 209 try: 210 return evaluator.evaluate(self.placeholder) 211 except Exception as exc: # pylint:disable=broad-except 212 wrapped = PropertyMappingExpressionException(exc, None) 213 LOGGER.warning( 214 "failed to evaluate prompt placeholder", 215 exc=wrapped, 216 ) 217 if dry_run: 218 raise wrapped from exc 219 return self.placeholder
Get fully interpolated placeholder
221 def get_initial_value( 222 self, 223 prompt_context: dict, 224 user: User, 225 request: HttpRequest, 226 dry_run: bool | None = False, 227 ) -> str: 228 """Get fully interpolated initial value""" 229 230 if self.field_key in prompt_context: 231 # We don't want to parse this as an expression since a user will 232 # be able to control the input 233 value = prompt_context[self.field_key] 234 elif self.initial_value_expression: 235 evaluator = PropertyMappingEvaluator( 236 self, user, request, prompt_context=prompt_context, dry_run=dry_run 237 ) 238 try: 239 value = evaluator.evaluate(self.initial_value) 240 except Exception as exc: # pylint:disable=broad-except 241 wrapped = PropertyMappingExpressionException(exc, None) 242 LOGGER.warning( 243 "failed to evaluate prompt initial value", 244 exc=wrapped, 245 ) 246 if dry_run: 247 raise wrapped from exc 248 value = self.initial_value 249 else: 250 value = self.initial_value 251 252 if self.type in CHOICE_FIELDS: 253 # Ensure returned value is a valid choice 254 choices = self.get_choices(prompt_context, user, request) 255 if not choices: 256 return "" 257 if not any( 258 choice.get("value") == value if isinstance(choice, dict) else choice == value 259 for choice in choices 260 ): 261 return choices[0] 262 263 return value
Get fully interpolated initial value
265 def field( # noqa PLR0915 266 self, default: Any | None, choices: list[Any] | None = None 267 ) -> CharField: 268 """Get field type for Challenge and response. Choices are only valid for CHOICE_FIELDS.""" 269 field_class = CharField 270 kwargs = { 271 "required": self.required, 272 } 273 match self.type: 274 case FieldTypes.TEXT | FieldTypes.TEXT_AREA: 275 kwargs["trim_whitespace"] = False 276 kwargs["allow_blank"] = not self.required 277 case FieldTypes.TEXT_READ_ONLY, FieldTypes.TEXT_AREA_READ_ONLY: 278 field_class = ReadOnlyField 279 # required can't be set for ReadOnlyField 280 kwargs["required"] = False 281 kwargs["allow_blank"] = True 282 case FieldTypes.EMAIL: 283 field_class = EmailField 284 kwargs["allow_blank"] = not self.required 285 case FieldTypes.NUMBER: 286 field_class = IntegerField 287 case FieldTypes.CHECKBOX: 288 field_class = BooleanField 289 kwargs["required"] = False 290 case FieldTypes.DATE: 291 field_class = DateField 292 case FieldTypes.DATE_TIME: 293 field_class = DateTimeField 294 case FieldTypes.FILE: 295 field_class = InlineFileField 296 case FieldTypes.SEPARATOR: 297 kwargs["required"] = False 298 kwargs["label"] = "" 299 case FieldTypes.HIDDEN: 300 field_class = HiddenField 301 kwargs["required"] = False 302 kwargs["default"] = self.placeholder 303 case FieldTypes.STATIC: 304 kwargs["default"] = self.placeholder 305 kwargs["required"] = False 306 kwargs["label"] = "" 307 308 case FieldTypes.AK_LOCALE: 309 kwargs["allow_blank"] = True 310 311 if self.type in CHOICE_FIELDS: 312 field_class = ChoiceField 313 kwargs["choices"] = [] 314 if choices: 315 for choice in choices: 316 label, value = choice, choice 317 if isinstance(choice, dict): 318 label = choice.get("label", "") 319 value = choice.get("value", "") 320 kwargs["choices"].append((value, label)) 321 322 if default: 323 kwargs["default"] = default 324 # May not set both `required` and `default` 325 if "default" in kwargs: 326 kwargs.pop("required", None) 327 return field_class(**kwargs)
Get field type for Challenge and response. Choices are only valid for CHOICE_FIELDS.
329 def save(self, *args, **kwargs): 330 if self.type not in FieldTypes: 331 raise ValueError 332 return super().save(*args, **kwargs)
Save the current instance. Override this in a subclass if you want to control the saving process.
The 'force_insert' and 'force_update' parameters can be used to insist that the "save" must be an SQL insert or update (or equivalent for non-SQL backends), respectively. Normally, they should not be set.
Method descriptor with partial application of the given arguments and keywords.
Supports wrapping existing descriptors and handles non-descriptor callables as instance methods.
Accessor to the related objects manager on the forward and reverse sides of a many-to-many relation.
In the example::
class Pizza(Model):
toppings = ManyToManyField(Topping, related_name='pizzas')
Pizza.toppings and Topping.pizzas are ManyToManyDescriptor
instances.
Most of the implementation is delegated to a dynamically defined manager
class built by create_forward_many_to_many_manager() defined below.
Inherited Members
The requested object does not exist
The query returned multiple objects when only one was expected.
342class PromptStage(Stage): 343 """Prompt the user to enter information.""" 344 345 fields = models.ManyToManyField(Prompt) 346 347 validation_policies = models.ManyToManyField(Policy, blank=True) 348 349 @property 350 def serializer(self) -> type[BaseSerializer]: 351 from authentik.stages.prompt.api import PromptStageSerializer 352 353 return PromptStageSerializer 354 355 @property 356 def view(self) -> type[View]: 357 from authentik.stages.prompt.stage import PromptStageView 358 359 return PromptStageView 360 361 @property 362 def component(self) -> str: 363 return "ak-stage-prompt-form" 364 365 class Meta: 366 verbose_name = _("Prompt Stage") 367 verbose_name_plural = _("Prompt Stages")
Prompt the user to enter information.
Accessor to the related objects manager on the forward and reverse sides of a many-to-many relation.
In the example::
class Pizza(Model):
toppings = ManyToManyField(Topping, related_name='pizzas')
Pizza.toppings and Topping.pizzas are ManyToManyDescriptor
instances.
Most of the implementation is delegated to a dynamically defined manager
class built by create_forward_many_to_many_manager() defined below.
Accessor to the related objects manager on the forward and reverse sides of a many-to-many relation.
In the example::
class Pizza(Model):
toppings = ManyToManyField(Topping, related_name='pizzas')
Pizza.toppings and Topping.pizzas are ManyToManyDescriptor
instances.
Most of the implementation is delegated to a dynamically defined manager
class built by create_forward_many_to_many_manager() defined below.
349 @property 350 def serializer(self) -> type[BaseSerializer]: 351 from authentik.stages.prompt.api import PromptStageSerializer 352 353 return PromptStageSerializer
Get serializer for this model
355 @property 356 def view(self) -> type[View]: 357 from authentik.stages.prompt.stage import PromptStageView 358 359 return PromptStageView
Return StageView class that implements logic for this stage
Accessor to the related object on the forward side of a one-to-one relation.
In the example::
class Restaurant(Model):
place = OneToOneField(Place, related_name='restaurant')
Restaurant.place is a ForwardOneToOneDescriptor instance.
Inherited Members
- authentik.flows.models.Stage
- stage_uuid
- name
- objects
- ui_user_settings
- is_in_memory
- flow_set
- flowstagebinding_set
- emailstage
- endpointstage
- invitationstage
- passwordstage
- promptstage
- authenticatorstaticstage
- authenticatorduostage
- authenticatoremailstage
- authenticatorsmsstage
- authenticatorwebauthnstage
- authenticatorvalidatestage
- captchastage
- identificationstage
- authenticatortotpstage
- consentstage
- denystage
- dummystage
- redirectstage
- userdeletestage
- userloginstage
- userlogoutstage
- userwritestage
- authenticatorendpointgdtcstage
- mutualtlsstage
- sourcestage
The requested object does not exist
The query returned multiple objects when only one was expected.