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")
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    # 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

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
ALERT_INFO = FieldTypes.ALERT_INFO
ALERT_WARNING = FieldTypes.ALERT_WARNING
ALERT_DANGER = FieldTypes.ALERT_DANGER
AK_LOCALE = FieldTypes.AK_LOCALE
class InlineFileField(rest_framework.fields.CharField):
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

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

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

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]
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

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:
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

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

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

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

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

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):
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.

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]
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

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

component: str
371    @property
372    def component(self) -> str:
373        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.