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")
CHOICES_CONTEXT_SUFFIX = '__choices'
LOGGER = <BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=())>
class FieldTypes(django.db.models.enums.TextChoices):
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

TEXT_AREA = FieldTypes.TEXT_AREA
TEXT_READ_ONLY = FieldTypes.TEXT_READ_ONLY
TEXT_AREA_READ_ONLY = FieldTypes.TEXT_AREA_READ_ONLY
USERNAME = FieldTypes.USERNAME
PASSWORD = FieldTypes.PASSWORD
CHECKBOX = FieldTypes.CHECKBOX
RADIO_BUTTON_GROUP = FieldTypes.RADIO_BUTTON_GROUP
DROPDOWN = FieldTypes.DROPDOWN
DATE_TIME = FieldTypes.DATE_TIME
SEPARATOR = FieldTypes.SEPARATOR
AK_LOCALE = FieldTypes.AK_LOCALE
class InlineFileField(rest_framework.fields.CharField):
 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

def to_internal_value(self, data: str):
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.

class Prompt(authentik.lib.models.SerializerModel):
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.

def prompt_uuid(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def name(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def field_key(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def label(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def type(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def required(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def placeholder(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def initial_value(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def sub_text(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def order(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def placeholder_expression(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def initial_value_expression(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

serializer: Type[rest_framework.serializers.BaseSerializer]
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

def get_choices( self, prompt_context: dict, user: authentik.core.models.User, request: django.http.request.HttpRequest, dry_run: bool | None = False) -> tuple[dict[str, Any]] | None:
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

def get_placeholder( self, prompt_context: dict, user: authentik.core.models.User, request: django.http.request.HttpRequest, dry_run: bool | None = False) -> str:
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

def get_initial_value( self, prompt_context: dict, user: authentik.core.models.User, request: django.http.request.HttpRequest, dry_run: bool | None = False) -> str:
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

def field( self, default: Any | None, choices: list[Any] | None = None) -> rest_framework.fields.CharField:
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.

def save(self, *args, **kwargs):
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.

def get_type_display(unknown):

Method descriptor with partial application of the given arguments and keywords.

Supports wrapping existing descriptors and handles non-descriptor callables as instance methods.

def objects(unknown):

The type of the None singleton.

promptstage_set

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.

class Prompt.DoesNotExist(django.core.exceptions.ObjectDoesNotExist):

The requested object does not exist

class Prompt.MultipleObjectsReturned(django.core.exceptions.MultipleObjectsReturned):

The query returned multiple objects when only one was expected.

class PromptStage(authentik.flows.models.Stage):
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.

fields

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.

validation_policies

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.

serializer: type[rest_framework.serializers.BaseSerializer]
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

view: type[django.views.generic.base.View]
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

component: str
361    @property
362    def component(self) -> str:
363        return "ak-stage-prompt-form"

Return component used to edit this object

stage_ptr_id
stage_ptr

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.

class PromptStage.DoesNotExist(authentik.flows.models.Stage.DoesNotExist):

The requested object does not exist

class PromptStage.MultipleObjectsReturned(authentik.flows.models.Stage.MultipleObjectsReturned):

The query returned multiple objects when only one was expected.