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 # Alert box types for displaying styled messages 91 ALERT_INFO = "alert_info", _("Alert (Info): Static alert box with info styling") 92 ALERT_WARNING = "alert_warning", _("Alert (Warning): Static alert box with warning styling") 93 ALERT_DANGER = "alert_danger", _("Alert (Danger): Static alert box with danger styling") 94 95 AK_LOCALE = "ak-locale", _("authentik: Selection of locales authentik supports") 96 97 98CHOICE_FIELDS = (FieldTypes.RADIO_BUTTON_GROUP, FieldTypes.DROPDOWN) 99 100 101class InlineFileField(CharField): 102 """Field for inline data-URI base64 encoded files""" 103 104 def to_internal_value(self, data: str): 105 uri = urlparse(data) 106 if uri.scheme != "data": 107 raise ValidationError("Invalid scheme") 108 header, _encoded = uri.path.split(",", 1) 109 _mime, _, enc = header.partition(";") 110 if enc != "base64": 111 raise ValidationError("Invalid encoding") 112 return super().to_internal_value(urlunparse(uri)) 113 114 115class Prompt(SerializerModel): 116 """Single Prompt, part of a prompt stage.""" 117 118 prompt_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) 119 name = models.TextField(unique=True, blank=False) 120 121 field_key = models.TextField( 122 help_text=_("Name of the form field, also used to store the value") 123 ) 124 label = models.TextField() 125 type = models.CharField(max_length=100, choices=FieldTypes.choices) 126 required = models.BooleanField(default=True) 127 placeholder = models.TextField( 128 blank=True, 129 help_text=_( 130 "Optionally provide a short hint that describes the expected input value. " 131 "When creating a fixed choice field, enable interpreting as " 132 "expression and return a list to return multiple choices." 133 ), 134 ) 135 initial_value = models.TextField( 136 blank=True, 137 help_text=_( 138 "Optionally pre-fill the input with an initial value. " 139 "When creating a fixed choice field, enable interpreting as " 140 "expression and return a list to return multiple default choices." 141 ), 142 ) 143 sub_text = models.TextField(blank=True, default="") 144 145 order = models.IntegerField(default=0) 146 147 placeholder_expression = models.BooleanField(default=False) 148 initial_value_expression = models.BooleanField(default=False) 149 150 @property 151 def serializer(self) -> Type[BaseSerializer]: # noqa: UP006 152 from authentik.stages.prompt.api import PromptSerializer 153 154 return PromptSerializer 155 156 def get_choices( 157 self, 158 prompt_context: dict, 159 user: User, 160 request: HttpRequest, 161 dry_run: bool | None = False, 162 ) -> tuple[dict[str, Any]] | None: 163 """Get fully interpolated list of choices""" 164 if self.type not in CHOICE_FIELDS: 165 return None 166 167 raw_choices = self.placeholder 168 169 if self.field_key + CHOICES_CONTEXT_SUFFIX in prompt_context: 170 raw_choices = prompt_context[self.field_key + CHOICES_CONTEXT_SUFFIX] 171 elif self.placeholder_expression: 172 evaluator = PropertyMappingEvaluator( 173 self, user, request, prompt_context=prompt_context, dry_run=dry_run 174 ) 175 try: 176 raw_choices = evaluator.evaluate(self.placeholder) 177 except Exception as exc: # pylint:disable=broad-except 178 wrapped = PropertyMappingExpressionException(exc, None) 179 LOGGER.warning( 180 "failed to evaluate prompt choices", 181 exc=wrapped, 182 ) 183 if dry_run: 184 raise wrapped from exc 185 186 if isinstance(raw_choices, list | tuple | set): 187 choices = raw_choices 188 else: 189 choices = [raw_choices] 190 191 if len(choices) == 0: 192 LOGGER.warning("failed to get prompt choices", choices=choices, input=raw_choices) 193 194 return tuple(choices) 195 196 def get_placeholder( 197 self, 198 prompt_context: dict, 199 user: User, 200 request: HttpRequest, 201 dry_run: bool | None = False, 202 ) -> str: 203 """Get fully interpolated placeholder""" 204 if self.type in CHOICE_FIELDS: 205 # Choice fields use the placeholder to define all valid choices. 206 # Therefore their actual placeholder is always blank 207 return "" 208 209 if self.placeholder_expression: 210 evaluator = PropertyMappingEvaluator( 211 self, user, request, prompt_context=prompt_context, dry_run=dry_run 212 ) 213 try: 214 return evaluator.evaluate(self.placeholder) 215 except Exception as exc: # pylint:disable=broad-except 216 wrapped = PropertyMappingExpressionException(exc, None) 217 LOGGER.warning( 218 "failed to evaluate prompt placeholder", 219 exc=wrapped, 220 ) 221 if dry_run: 222 raise wrapped from exc 223 return self.placeholder 224 225 def get_initial_value( 226 self, 227 prompt_context: dict, 228 user: User, 229 request: HttpRequest, 230 dry_run: bool | None = False, 231 ) -> str: 232 """Get fully interpolated initial value""" 233 234 if self.field_key in prompt_context: 235 # We don't want to parse this as an expression since a user will 236 # be able to control the input 237 value = prompt_context[self.field_key] 238 elif self.initial_value_expression: 239 evaluator = PropertyMappingEvaluator( 240 self, user, request, prompt_context=prompt_context, dry_run=dry_run 241 ) 242 try: 243 value = evaluator.evaluate(self.initial_value) 244 except Exception as exc: # pylint:disable=broad-except 245 wrapped = PropertyMappingExpressionException(exc, None) 246 LOGGER.warning( 247 "failed to evaluate prompt initial value", 248 exc=wrapped, 249 ) 250 if dry_run: 251 raise wrapped from exc 252 value = self.initial_value 253 else: 254 value = self.initial_value 255 256 if self.type in CHOICE_FIELDS: 257 # Ensure returned value is a valid choice 258 choices = self.get_choices(prompt_context, user, request) 259 if not choices: 260 return "" 261 if not any( 262 choice.get("value") == value if isinstance(choice, dict) else choice == value 263 for choice in choices 264 ): 265 return choices[0] 266 267 return value 268 269 def field( # noqa PLR0915 270 self, default: Any | None, choices: list[Any] | None = None 271 ) -> CharField: 272 """Get field type for Challenge and response. Choices are only valid for CHOICE_FIELDS.""" 273 field_class = CharField 274 kwargs = { 275 "required": self.required, 276 } 277 match self.type: 278 case FieldTypes.TEXT | FieldTypes.TEXT_AREA: 279 kwargs["trim_whitespace"] = False 280 kwargs["allow_blank"] = not self.required 281 case FieldTypes.TEXT_READ_ONLY, FieldTypes.TEXT_AREA_READ_ONLY: 282 field_class = ReadOnlyField 283 # required can't be set for ReadOnlyField 284 kwargs["required"] = False 285 kwargs["allow_blank"] = True 286 case FieldTypes.EMAIL: 287 field_class = EmailField 288 kwargs["allow_blank"] = not self.required 289 case FieldTypes.NUMBER: 290 field_class = IntegerField 291 case FieldTypes.CHECKBOX: 292 field_class = BooleanField 293 kwargs["required"] = False 294 case FieldTypes.DATE: 295 field_class = DateField 296 case FieldTypes.DATE_TIME: 297 field_class = DateTimeField 298 case FieldTypes.FILE: 299 field_class = InlineFileField 300 case FieldTypes.SEPARATOR: 301 kwargs["required"] = False 302 kwargs["label"] = "" 303 case FieldTypes.HIDDEN: 304 field_class = HiddenField 305 kwargs["required"] = False 306 kwargs["default"] = self.placeholder 307 case ( 308 FieldTypes.STATIC 309 | FieldTypes.ALERT_INFO 310 | FieldTypes.ALERT_WARNING 311 | FieldTypes.ALERT_DANGER 312 ): 313 kwargs["default"] = self.placeholder 314 kwargs["required"] = False 315 kwargs["label"] = "" 316 317 case FieldTypes.AK_LOCALE: 318 kwargs["allow_blank"] = True 319 320 if self.type in CHOICE_FIELDS: 321 field_class = ChoiceField 322 kwargs["choices"] = [] 323 if choices: 324 for choice in choices: 325 label, value = choice, choice 326 if isinstance(choice, dict): 327 label = choice.get("label", "") 328 value = choice.get("value", "") 329 kwargs["choices"].append((value, label)) 330 331 if default: 332 kwargs["default"] = default 333 # May not set both `required` and `default` 334 if "default" in kwargs: 335 kwargs.pop("required", None) 336 return field_class(**kwargs) 337 338 def save(self, *args, **kwargs): 339 if self.type not in FieldTypes: 340 raise ValueError 341 return super().save(*args, **kwargs) 342 343 def __str__(self): 344 return f"Prompt field '{self.field_key}' type {self.type}" 345 346 class Meta: 347 verbose_name = _("Prompt") 348 verbose_name_plural = _("Prompts") 349 350 351class PromptStage(Stage): 352 """Prompt the user to enter information.""" 353 354 fields = models.ManyToManyField(Prompt) 355 356 validation_policies = models.ManyToManyField(Policy, blank=True) 357 358 @property 359 def serializer(self) -> type[BaseSerializer]: 360 from authentik.stages.prompt.api import PromptStageSerializer 361 362 return PromptStageSerializer 363 364 @property 365 def view(self) -> type[View]: 366 from authentik.stages.prompt.stage import PromptStageView 367 368 return PromptStageView 369 370 @property 371 def component(self) -> str: 372 return "ak-stage-prompt-form" 373 374 class Meta: 375 verbose_name = _("Prompt Stage") 376 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 # Alert box types for displaying styled messages 92 ALERT_INFO = "alert_info", _("Alert (Info): Static alert box with info styling") 93 ALERT_WARNING = "alert_warning", _("Alert (Warning): Static alert box with warning styling") 94 ALERT_DANGER = "alert_danger", _("Alert (Danger): Static alert box with danger styling") 95 96 AK_LOCALE = "ak-locale", _("authentik: Selection of locales authentik supports")
Field types an Prompt can be
102class InlineFileField(CharField): 103 """Field for inline data-URI base64 encoded files""" 104 105 def to_internal_value(self, data: str): 106 uri = urlparse(data) 107 if uri.scheme != "data": 108 raise ValidationError("Invalid scheme") 109 header, _encoded = uri.path.split(",", 1) 110 _mime, _, enc = header.partition(";") 111 if enc != "base64": 112 raise ValidationError("Invalid encoding") 113 return super().to_internal_value(urlunparse(uri))
Field for inline data-URI base64 encoded files
105 def to_internal_value(self, data: str): 106 uri = urlparse(data) 107 if uri.scheme != "data": 108 raise ValidationError("Invalid scheme") 109 header, _encoded = uri.path.split(",", 1) 110 _mime, _, enc = header.partition(";") 111 if enc != "base64": 112 raise ValidationError("Invalid encoding") 113 return super().to_internal_value(urlunparse(uri))
Transform the incoming primitive data into a native value.
116class Prompt(SerializerModel): 117 """Single Prompt, part of a prompt stage.""" 118 119 prompt_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) 120 name = models.TextField(unique=True, blank=False) 121 122 field_key = models.TextField( 123 help_text=_("Name of the form field, also used to store the value") 124 ) 125 label = models.TextField() 126 type = models.CharField(max_length=100, choices=FieldTypes.choices) 127 required = models.BooleanField(default=True) 128 placeholder = models.TextField( 129 blank=True, 130 help_text=_( 131 "Optionally provide a short hint that describes the expected input value. " 132 "When creating a fixed choice field, enable interpreting as " 133 "expression and return a list to return multiple choices." 134 ), 135 ) 136 initial_value = models.TextField( 137 blank=True, 138 help_text=_( 139 "Optionally pre-fill the input with an initial value. " 140 "When creating a fixed choice field, enable interpreting as " 141 "expression and return a list to return multiple default choices." 142 ), 143 ) 144 sub_text = models.TextField(blank=True, default="") 145 146 order = models.IntegerField(default=0) 147 148 placeholder_expression = models.BooleanField(default=False) 149 initial_value_expression = models.BooleanField(default=False) 150 151 @property 152 def serializer(self) -> Type[BaseSerializer]: # noqa: UP006 153 from authentik.stages.prompt.api import PromptSerializer 154 155 return PromptSerializer 156 157 def get_choices( 158 self, 159 prompt_context: dict, 160 user: User, 161 request: HttpRequest, 162 dry_run: bool | None = False, 163 ) -> tuple[dict[str, Any]] | None: 164 """Get fully interpolated list of choices""" 165 if self.type not in CHOICE_FIELDS: 166 return None 167 168 raw_choices = self.placeholder 169 170 if self.field_key + CHOICES_CONTEXT_SUFFIX in prompt_context: 171 raw_choices = prompt_context[self.field_key + CHOICES_CONTEXT_SUFFIX] 172 elif self.placeholder_expression: 173 evaluator = PropertyMappingEvaluator( 174 self, user, request, prompt_context=prompt_context, dry_run=dry_run 175 ) 176 try: 177 raw_choices = evaluator.evaluate(self.placeholder) 178 except Exception as exc: # pylint:disable=broad-except 179 wrapped = PropertyMappingExpressionException(exc, None) 180 LOGGER.warning( 181 "failed to evaluate prompt choices", 182 exc=wrapped, 183 ) 184 if dry_run: 185 raise wrapped from exc 186 187 if isinstance(raw_choices, list | tuple | set): 188 choices = raw_choices 189 else: 190 choices = [raw_choices] 191 192 if len(choices) == 0: 193 LOGGER.warning("failed to get prompt choices", choices=choices, input=raw_choices) 194 195 return tuple(choices) 196 197 def get_placeholder( 198 self, 199 prompt_context: dict, 200 user: User, 201 request: HttpRequest, 202 dry_run: bool | None = False, 203 ) -> str: 204 """Get fully interpolated placeholder""" 205 if self.type in CHOICE_FIELDS: 206 # Choice fields use the placeholder to define all valid choices. 207 # Therefore their actual placeholder is always blank 208 return "" 209 210 if self.placeholder_expression: 211 evaluator = PropertyMappingEvaluator( 212 self, user, request, prompt_context=prompt_context, dry_run=dry_run 213 ) 214 try: 215 return evaluator.evaluate(self.placeholder) 216 except Exception as exc: # pylint:disable=broad-except 217 wrapped = PropertyMappingExpressionException(exc, None) 218 LOGGER.warning( 219 "failed to evaluate prompt placeholder", 220 exc=wrapped, 221 ) 222 if dry_run: 223 raise wrapped from exc 224 return self.placeholder 225 226 def get_initial_value( 227 self, 228 prompt_context: dict, 229 user: User, 230 request: HttpRequest, 231 dry_run: bool | None = False, 232 ) -> str: 233 """Get fully interpolated initial value""" 234 235 if self.field_key in prompt_context: 236 # We don't want to parse this as an expression since a user will 237 # be able to control the input 238 value = prompt_context[self.field_key] 239 elif self.initial_value_expression: 240 evaluator = PropertyMappingEvaluator( 241 self, user, request, prompt_context=prompt_context, dry_run=dry_run 242 ) 243 try: 244 value = evaluator.evaluate(self.initial_value) 245 except Exception as exc: # pylint:disable=broad-except 246 wrapped = PropertyMappingExpressionException(exc, None) 247 LOGGER.warning( 248 "failed to evaluate prompt initial value", 249 exc=wrapped, 250 ) 251 if dry_run: 252 raise wrapped from exc 253 value = self.initial_value 254 else: 255 value = self.initial_value 256 257 if self.type in CHOICE_FIELDS: 258 # Ensure returned value is a valid choice 259 choices = self.get_choices(prompt_context, user, request) 260 if not choices: 261 return "" 262 if not any( 263 choice.get("value") == value if isinstance(choice, dict) else choice == value 264 for choice in choices 265 ): 266 return choices[0] 267 268 return value 269 270 def field( # noqa PLR0915 271 self, default: Any | None, choices: list[Any] | None = None 272 ) -> CharField: 273 """Get field type for Challenge and response. Choices are only valid for CHOICE_FIELDS.""" 274 field_class = CharField 275 kwargs = { 276 "required": self.required, 277 } 278 match self.type: 279 case FieldTypes.TEXT | FieldTypes.TEXT_AREA: 280 kwargs["trim_whitespace"] = False 281 kwargs["allow_blank"] = not self.required 282 case FieldTypes.TEXT_READ_ONLY, FieldTypes.TEXT_AREA_READ_ONLY: 283 field_class = ReadOnlyField 284 # required can't be set for ReadOnlyField 285 kwargs["required"] = False 286 kwargs["allow_blank"] = True 287 case FieldTypes.EMAIL: 288 field_class = EmailField 289 kwargs["allow_blank"] = not self.required 290 case FieldTypes.NUMBER: 291 field_class = IntegerField 292 case FieldTypes.CHECKBOX: 293 field_class = BooleanField 294 kwargs["required"] = False 295 case FieldTypes.DATE: 296 field_class = DateField 297 case FieldTypes.DATE_TIME: 298 field_class = DateTimeField 299 case FieldTypes.FILE: 300 field_class = InlineFileField 301 case FieldTypes.SEPARATOR: 302 kwargs["required"] = False 303 kwargs["label"] = "" 304 case FieldTypes.HIDDEN: 305 field_class = HiddenField 306 kwargs["required"] = False 307 kwargs["default"] = self.placeholder 308 case ( 309 FieldTypes.STATIC 310 | FieldTypes.ALERT_INFO 311 | FieldTypes.ALERT_WARNING 312 | FieldTypes.ALERT_DANGER 313 ): 314 kwargs["default"] = self.placeholder 315 kwargs["required"] = False 316 kwargs["label"] = "" 317 318 case FieldTypes.AK_LOCALE: 319 kwargs["allow_blank"] = True 320 321 if self.type in CHOICE_FIELDS: 322 field_class = ChoiceField 323 kwargs["choices"] = [] 324 if choices: 325 for choice in choices: 326 label, value = choice, choice 327 if isinstance(choice, dict): 328 label = choice.get("label", "") 329 value = choice.get("value", "") 330 kwargs["choices"].append((value, label)) 331 332 if default: 333 kwargs["default"] = default 334 # May not set both `required` and `default` 335 if "default" in kwargs: 336 kwargs.pop("required", None) 337 return field_class(**kwargs) 338 339 def save(self, *args, **kwargs): 340 if self.type not in FieldTypes: 341 raise ValueError 342 return super().save(*args, **kwargs) 343 344 def __str__(self): 345 return f"Prompt field '{self.field_key}' type {self.type}" 346 347 class Meta: 348 verbose_name = _("Prompt") 349 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.
151 @property 152 def serializer(self) -> Type[BaseSerializer]: # noqa: UP006 153 from authentik.stages.prompt.api import PromptSerializer 154 155 return PromptSerializer
Get serializer for this model
157 def get_choices( 158 self, 159 prompt_context: dict, 160 user: User, 161 request: HttpRequest, 162 dry_run: bool | None = False, 163 ) -> tuple[dict[str, Any]] | None: 164 """Get fully interpolated list of choices""" 165 if self.type not in CHOICE_FIELDS: 166 return None 167 168 raw_choices = self.placeholder 169 170 if self.field_key + CHOICES_CONTEXT_SUFFIX in prompt_context: 171 raw_choices = prompt_context[self.field_key + CHOICES_CONTEXT_SUFFIX] 172 elif self.placeholder_expression: 173 evaluator = PropertyMappingEvaluator( 174 self, user, request, prompt_context=prompt_context, dry_run=dry_run 175 ) 176 try: 177 raw_choices = evaluator.evaluate(self.placeholder) 178 except Exception as exc: # pylint:disable=broad-except 179 wrapped = PropertyMappingExpressionException(exc, None) 180 LOGGER.warning( 181 "failed to evaluate prompt choices", 182 exc=wrapped, 183 ) 184 if dry_run: 185 raise wrapped from exc 186 187 if isinstance(raw_choices, list | tuple | set): 188 choices = raw_choices 189 else: 190 choices = [raw_choices] 191 192 if len(choices) == 0: 193 LOGGER.warning("failed to get prompt choices", choices=choices, input=raw_choices) 194 195 return tuple(choices)
Get fully interpolated list of choices
197 def get_placeholder( 198 self, 199 prompt_context: dict, 200 user: User, 201 request: HttpRequest, 202 dry_run: bool | None = False, 203 ) -> str: 204 """Get fully interpolated placeholder""" 205 if self.type in CHOICE_FIELDS: 206 # Choice fields use the placeholder to define all valid choices. 207 # Therefore their actual placeholder is always blank 208 return "" 209 210 if self.placeholder_expression: 211 evaluator = PropertyMappingEvaluator( 212 self, user, request, prompt_context=prompt_context, dry_run=dry_run 213 ) 214 try: 215 return evaluator.evaluate(self.placeholder) 216 except Exception as exc: # pylint:disable=broad-except 217 wrapped = PropertyMappingExpressionException(exc, None) 218 LOGGER.warning( 219 "failed to evaluate prompt placeholder", 220 exc=wrapped, 221 ) 222 if dry_run: 223 raise wrapped from exc 224 return self.placeholder
Get fully interpolated placeholder
226 def get_initial_value( 227 self, 228 prompt_context: dict, 229 user: User, 230 request: HttpRequest, 231 dry_run: bool | None = False, 232 ) -> str: 233 """Get fully interpolated initial value""" 234 235 if self.field_key in prompt_context: 236 # We don't want to parse this as an expression since a user will 237 # be able to control the input 238 value = prompt_context[self.field_key] 239 elif self.initial_value_expression: 240 evaluator = PropertyMappingEvaluator( 241 self, user, request, prompt_context=prompt_context, dry_run=dry_run 242 ) 243 try: 244 value = evaluator.evaluate(self.initial_value) 245 except Exception as exc: # pylint:disable=broad-except 246 wrapped = PropertyMappingExpressionException(exc, None) 247 LOGGER.warning( 248 "failed to evaluate prompt initial value", 249 exc=wrapped, 250 ) 251 if dry_run: 252 raise wrapped from exc 253 value = self.initial_value 254 else: 255 value = self.initial_value 256 257 if self.type in CHOICE_FIELDS: 258 # Ensure returned value is a valid choice 259 choices = self.get_choices(prompt_context, user, request) 260 if not choices: 261 return "" 262 if not any( 263 choice.get("value") == value if isinstance(choice, dict) else choice == value 264 for choice in choices 265 ): 266 return choices[0] 267 268 return value
Get fully interpolated initial value
270 def field( # noqa PLR0915 271 self, default: Any | None, choices: list[Any] | None = None 272 ) -> CharField: 273 """Get field type for Challenge and response. Choices are only valid for CHOICE_FIELDS.""" 274 field_class = CharField 275 kwargs = { 276 "required": self.required, 277 } 278 match self.type: 279 case FieldTypes.TEXT | FieldTypes.TEXT_AREA: 280 kwargs["trim_whitespace"] = False 281 kwargs["allow_blank"] = not self.required 282 case FieldTypes.TEXT_READ_ONLY, FieldTypes.TEXT_AREA_READ_ONLY: 283 field_class = ReadOnlyField 284 # required can't be set for ReadOnlyField 285 kwargs["required"] = False 286 kwargs["allow_blank"] = True 287 case FieldTypes.EMAIL: 288 field_class = EmailField 289 kwargs["allow_blank"] = not self.required 290 case FieldTypes.NUMBER: 291 field_class = IntegerField 292 case FieldTypes.CHECKBOX: 293 field_class = BooleanField 294 kwargs["required"] = False 295 case FieldTypes.DATE: 296 field_class = DateField 297 case FieldTypes.DATE_TIME: 298 field_class = DateTimeField 299 case FieldTypes.FILE: 300 field_class = InlineFileField 301 case FieldTypes.SEPARATOR: 302 kwargs["required"] = False 303 kwargs["label"] = "" 304 case FieldTypes.HIDDEN: 305 field_class = HiddenField 306 kwargs["required"] = False 307 kwargs["default"] = self.placeholder 308 case ( 309 FieldTypes.STATIC 310 | FieldTypes.ALERT_INFO 311 | FieldTypes.ALERT_WARNING 312 | FieldTypes.ALERT_DANGER 313 ): 314 kwargs["default"] = self.placeholder 315 kwargs["required"] = False 316 kwargs["label"] = "" 317 318 case FieldTypes.AK_LOCALE: 319 kwargs["allow_blank"] = True 320 321 if self.type in CHOICE_FIELDS: 322 field_class = ChoiceField 323 kwargs["choices"] = [] 324 if choices: 325 for choice in choices: 326 label, value = choice, choice 327 if isinstance(choice, dict): 328 label = choice.get("label", "") 329 value = choice.get("value", "") 330 kwargs["choices"].append((value, label)) 331 332 if default: 333 kwargs["default"] = default 334 # May not set both `required` and `default` 335 if "default" in kwargs: 336 kwargs.pop("required", None) 337 return field_class(**kwargs)
Get field type for Challenge and response. Choices are only valid for CHOICE_FIELDS.
339 def save(self, *args, **kwargs): 340 if self.type not in FieldTypes: 341 raise ValueError 342 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.
352class PromptStage(Stage): 353 """Prompt the user to enter information.""" 354 355 fields = models.ManyToManyField(Prompt) 356 357 validation_policies = models.ManyToManyField(Policy, blank=True) 358 359 @property 360 def serializer(self) -> type[BaseSerializer]: 361 from authentik.stages.prompt.api import PromptStageSerializer 362 363 return PromptStageSerializer 364 365 @property 366 def view(self) -> type[View]: 367 from authentik.stages.prompt.stage import PromptStageView 368 369 return PromptStageView 370 371 @property 372 def component(self) -> str: 373 return "ak-stage-prompt-form" 374 375 class Meta: 376 verbose_name = _("Prompt Stage") 377 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.
359 @property 360 def serializer(self) -> type[BaseSerializer]: 361 from authentik.stages.prompt.api import PromptStageSerializer 362 363 return PromptStageSerializer
Get serializer for this model
365 @property 366 def view(self) -> type[View]: 367 from authentik.stages.prompt.stage import PromptStageView 368 369 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
- accountlockdownstage
- authenticatorendpointgdtcstage
- mutualtlsstage
- sourcestage
The requested object does not exist
The query returned multiple objects when only one was expected.