stages/prompt: Add Radio Button Group, Dropdown and Text Area prompt fields (#4822)

* Added radio-button prompt type in model

* Add radio-button prompt

* Refactored radio-button prompt; Added dropdown prompt

* Added tests

* Fixed unrelated to choice fields bug causing validation errors; Added more tests

* Added description for new prompts

* Added docs

* Fix lint

* Add forgotten file changes

* Fix lint

* Small fix

* Add text-area prompts

* Update authentik/stages/prompt/models.py

Co-authored-by: Jens L. <jens@beryju.org>
Signed-off-by: sdimovv <36302090+sdimovv@users.noreply.github.com>

* Update authentik/stages/prompt/models.py

Co-authored-by: Jens L. <jens@beryju.org>
Signed-off-by: sdimovv <36302090+sdimovv@users.noreply.github.com>

* Fix inline css

* remove AKGlobal, update schema

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: sdimovv <36302090+sdimovv@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@beryju.org>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
sdimovv 2023-03-19 19:56:17 +02:00 committed by GitHub
parent 4da18b5f0c
commit 8b52d711e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 475 additions and 30 deletions

View File

@ -54,6 +54,7 @@ class TestPasswordPolicyFlow(FlowTestCase):
component="ak-stage-prompt",
fields=[
{
"choices": None,
"field_key": "password",
"label": "PASSWORD_LABEL",
"order": 0,

View File

@ -0,0 +1,65 @@
# Generated by Django 4.1.7 on 2023-03-03 17:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_prompt", "0009_prompt_name"),
]
operations = [
migrations.AlterField(
model_name="prompt",
name="placeholder",
field=models.TextField(
blank=True,
help_text="When creating a Radio Button Group or Dropdown, enable interpreting as expression and return a list to return multiple choices.",
),
),
migrations.AlterField(
model_name="prompt",
name="type",
field=models.CharField(
choices=[
("text", "Text: Simple Text input"),
("text_area", "Text area: Multiline Text Input."),
(
"text_read_only",
"Text (read-only): Simple Text input, but cannot be edited.",
),
(
"text_area_read_only",
"Text area (read-only): Multiline Text input, but cannot be edited.",
),
(
"username",
"Username: Same as Text input, but checks for and prevents duplicate usernames.",
),
("email", "Email: Text field with Email type."),
(
"password",
"Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical.",
),
("number", "Number"),
("checkbox", "Checkbox"),
(
"radio-button-group",
"Fixed choice field rendered as a group of radio buttons.",
),
("dropdown", "Fixed choice field rendered as a dropdown."),
("date", "Date"),
("date-time", "Date Time"),
(
"file",
"File: File upload for arbitrary files. File content will be available in flow context as data-URI",
),
("separator", "Separator: Static Separator Line"),
("hidden", "Hidden: Hidden field, can be used to insert data into form."),
("static", "Static: Static value, displayed as-is."),
("ak-locale", "authentik: Selection of locales authentik supports"),
],
max_length=100,
),
),
]

View File

@ -11,6 +11,7 @@ from rest_framework.exceptions import ValidationError
from rest_framework.fields import (
BooleanField,
CharField,
ChoiceField,
DateField,
DateTimeField,
EmailField,
@ -38,10 +39,17 @@ class FieldTypes(models.TextChoices):
# Simple text field
TEXT = "text", _("Text: Simple Text input")
# Long text field
TEXT_AREA = "text_area", _("Text area: Multiline Text Input.")
# Simple text field
TEXT_READ_ONLY = "text_read_only", _(
"Text (read-only): Simple Text input, but cannot be edited."
)
# Long text field
TEXT_AREA_READ_ONLY = "text_area_read_only", _(
"Text area (read-only): Multiline Text input, but cannot be edited."
)
# Same as text, but has autocomplete for password managers
USERNAME = (
"username",
@ -58,6 +66,10 @@ class FieldTypes(models.TextChoices):
)
NUMBER = "number"
CHECKBOX = "checkbox"
RADIO_BUTTON_GROUP = "radio-button-group", _(
"Fixed choice field rendered as a group of radio buttons."
)
DROPDOWN = "dropdown", _("Fixed choice field rendered as a dropdown.")
DATE = "date"
DATE_TIME = "date-time"
@ -76,6 +88,9 @@ class FieldTypes(models.TextChoices):
AK_LOCALE = "ak-locale", _("authentik: Selection of locales authentik supports")
CHOICE_FIELDS = (FieldTypes.RADIO_BUTTON_GROUP, FieldTypes.DROPDOWN)
class InlineFileField(CharField):
"""Field for inline data-URI base64 encoded files"""
@ -102,7 +117,13 @@ class Prompt(SerializerModel):
label = models.TextField()
type = models.CharField(max_length=100, choices=FieldTypes.choices)
required = models.BooleanField(default=True)
placeholder = models.TextField(blank=True)
placeholder = models.TextField(
blank=True,
help_text=_(
"When creating a Radio Button Group or Dropdown, enable interpreting as "
"expression and return a list to return multiple choices."
),
)
sub_text = models.TextField(blank=True, default="")
order = models.IntegerField(default=0)
@ -115,8 +136,46 @@ class Prompt(SerializerModel):
return PromptSerializer
def get_choices(
self, prompt_context: dict, user: User, request: HttpRequest
) -> Optional[tuple[dict[str, Any]]]:
"""Get fully interpolated list of choices"""
if self.type not in CHOICE_FIELDS:
return None
raw_choices = self.placeholder
if self.field_key in prompt_context:
raw_choices = prompt_context[self.field_key]
elif self.placeholder_expression:
evaluator = PropertyMappingEvaluator(self, user, request, prompt_context=prompt_context)
try:
raw_choices = evaluator.evaluate(self.placeholder)
except Exception as exc: # pylint:disable=broad-except
LOGGER.warning(
"failed to evaluate prompt choices",
exc=PropertyMappingExpressionException(str(exc)),
)
if isinstance(raw_choices, (list, tuple, set)):
choices = raw_choices
else:
choices = [raw_choices]
if len(choices) == 0:
LOGGER.warning("failed to get prompt choices", choices=choices, input=raw_choices)
return tuple(choices)
def get_placeholder(self, prompt_context: dict, user: User, request: HttpRequest) -> str:
"""Get fully interpolated placeholder"""
if self.type in CHOICE_FIELDS:
# Make sure to return a valid choice as placeholder
choices = self.get_choices(prompt_context, user, request)
if not choices:
return ""
return choices[0]
if self.field_key in prompt_context:
# We don't want to parse this as an expression since a user will
# be able to control the input
@ -133,16 +192,16 @@ class Prompt(SerializerModel):
)
return self.placeholder
def field(self, default: Optional[Any]) -> CharField:
"""Get field type for Challenge and response"""
def field(self, default: Optional[Any], choices: Optional[list[Any]] = None) -> CharField:
"""Get field type for Challenge and response. Choices are only valid for CHOICE_FIELDS."""
field_class = CharField
kwargs = {
"required": self.required,
}
if self.type == FieldTypes.TEXT:
if self.type in (FieldTypes.TEXT, FieldTypes.TEXT_AREA):
kwargs["trim_whitespace"] = False
kwargs["allow_blank"] = not self.required
if self.type == FieldTypes.TEXT_READ_ONLY:
if self.type in (FieldTypes.TEXT_READ_ONLY, FieldTypes.TEXT_AREA_READ_ONLY):
field_class = ReadOnlyField
# required can't be set for ReadOnlyField
kwargs["required"] = False
@ -154,13 +213,15 @@ class Prompt(SerializerModel):
if self.type == FieldTypes.CHECKBOX:
field_class = BooleanField
kwargs["required"] = False
if self.type in CHOICE_FIELDS:
field_class = ChoiceField
kwargs["choices"] = choices or []
if self.type == FieldTypes.DATE:
field_class = DateField
if self.type == FieldTypes.DATE_TIME:
field_class = DateTimeField
if self.type == FieldTypes.FILE:
field_class = InlineFileField
if self.type == FieldTypes.SEPARATOR:
kwargs["required"] = False
kwargs["label"] = ""

View File

@ -7,7 +7,14 @@ from django.db.models.query import QuerySet
from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict
from django.utils.translation import gettext_lazy as _
from rest_framework.fields import BooleanField, CharField, ChoiceField, IntegerField, empty
from rest_framework.fields import (
BooleanField,
CharField,
ChoiceField,
IntegerField,
ListField,
empty,
)
from rest_framework.serializers import ValidationError
from authentik.core.api.utils import PassiveSerializer
@ -33,6 +40,7 @@ class StagePromptSerializer(PassiveSerializer):
placeholder = CharField(allow_blank=True)
order = IntegerField()
sub_text = CharField(allow_blank=True)
choices = ListField(child=CharField(allow_blank=True), allow_empty=True, allow_null=True)
class PromptChallenge(Challenge):
@ -65,10 +73,13 @@ class PromptChallengeResponse(ChallengeResponse):
fields = list(self.stage_instance.fields.all())
for field in fields:
field: Prompt
choices = field.get_choices(
plan.context.get(PLAN_CONTEXT_PROMPT, {}), user, self.request
)
current = field.get_placeholder(
plan.context.get(PLAN_CONTEXT_PROMPT, {}), user, self.request
)
self.fields[field.field_key] = field.field(current)
self.fields[field.field_key] = field.field(current, choices)
# Special handling for fields with username type
# these check for existing users with the same username
if field.type == FieldTypes.USERNAME:
@ -99,7 +110,12 @@ class PromptChallengeResponse(ChallengeResponse):
# Check if we have any static or hidden fields, and ensure they
# still have the same value
static_hidden_fields: QuerySet[Prompt] = self.stage_instance.fields.filter(
type__in=[FieldTypes.HIDDEN, FieldTypes.STATIC, FieldTypes.TEXT_READ_ONLY]
type__in=[
FieldTypes.HIDDEN,
FieldTypes.STATIC,
FieldTypes.TEXT_READ_ONLY,
FieldTypes.TEXT_AREA_READ_ONLY,
]
)
for static_hidden in static_hidden_fields:
field = self.fields[static_hidden.field_key]
@ -180,8 +196,15 @@ class PromptStageView(ChallengeStageView):
context_prompt = self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {})
for field in fields:
data = StagePromptSerializer(field).data
data["placeholder"] = field.get_placeholder(
context_prompt, self.get_pending_user(), self.request
# Ensure all choices and placeholders are str, as otherwise further in
# we can fail serializer validation if we return some types such as bool
choices = field.get_choices(context_prompt, self.get_pending_user(), self.request)
if choices:
data["choices"] = [str(choice) for choice in choices]
else:
data["choices"] = None
data["placeholder"] = str(
field.get_placeholder(context_prompt, self.get_pending_user(), self.request)
)
serializers.append(data)
challenge = PromptChallenge(

View File

@ -45,6 +45,14 @@ class TestPromptStage(FlowTestCase):
required=True,
placeholder="TEXT_PLACEHOLDER",
)
text_area_prompt = Prompt.objects.create(
name=generate_id(),
field_key="text_area_prompt",
label="TEXT_AREA_LABEL",
type=FieldTypes.TEXT_AREA,
required=True,
placeholder="TEXT_AREA_PLACEHOLDER",
)
email_prompt = Prompt.objects.create(
name=generate_id(),
field_key="email_prompt",
@ -91,6 +99,19 @@ class TestPromptStage(FlowTestCase):
required=True,
placeholder="static",
)
radio_button_group = Prompt.objects.create(
name=generate_id(),
field_key="radio_button_group",
type=FieldTypes.RADIO_BUTTON_GROUP,
required=True,
placeholder="test",
)
dropdown = Prompt.objects.create(
name=generate_id(),
field_key="dropdown",
type=FieldTypes.DROPDOWN,
required=True,
)
self.stage = PromptStage.objects.create(name="prompt-stage")
self.stage.fields.set(
[
@ -102,18 +123,23 @@ class TestPromptStage(FlowTestCase):
number_prompt,
hidden_prompt,
static_prompt,
radio_button_group,
dropdown,
]
)
self.prompt_data = {
username_prompt.field_key: "test-username",
text_prompt.field_key: "test-input",
text_area_prompt.field_key: "test-area-input",
email_prompt.field_key: "test@test.test",
password_prompt.field_key: "test",
password2_prompt.field_key: "test",
number_prompt.field_key: 3,
hidden_prompt.field_key: hidden_prompt.placeholder,
static_prompt.field_key: static_prompt.placeholder,
radio_button_group.field_key: radio_button_group.placeholder,
dropdown.field_key: "",
}
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
@ -251,6 +277,34 @@ class TestPromptStage(FlowTestCase):
{"username_prompt": [ErrorDetail(string="Username is already taken.", code="invalid")]},
)
def test_invalid_choice_field(self):
"""Test invalid choice field value"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
self.prompt_data["radio_button_group"] = "some invalid choice"
self.prompt_data["dropdown"] = "another invalid choice"
challenge_response = PromptChallengeResponse(
None, stage_instance=self.stage, plan=plan, data=self.prompt_data, stage=self.stage_view
)
self.assertEqual(challenge_response.is_valid(), False)
self.assertEqual(
challenge_response.errors,
{
"radio_button_group": [
ErrorDetail(
string=f"\"{self.prompt_data['radio_button_group']}\" "
"is not a valid choice.",
code="invalid_choice",
)
],
"dropdown": [
ErrorDetail(
string=f"\"{self.prompt_data['dropdown']}\" is not a valid choice.",
code="invalid_choice",
)
],
},
)
def test_static_hidden_overwrite(self):
"""Test that static and hidden fields ignore any value sent to them"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
@ -289,6 +343,113 @@ class TestPromptStage(FlowTestCase):
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
def test_choice_prompts_placeholders(self):
"""Test placeholders and expression of choice fields"""
context = {"foo": generate_id()}
# No choices - unusable (in the sense it creates an unsubmittable form)
# but valid behaviour
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.RADIO_BUTTON_GROUP,
placeholder="return []",
placeholder_expression=True,
)
self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
self.assertEqual(prompt.get_choices(context, self.user, self.factory.get("/")), tuple())
context["fixed_choice_prompt_expression"] = generate_id()
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")),
context["fixed_choice_prompt_expression"],
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")),
(context["fixed_choice_prompt_expression"],),
)
self.assertNotEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
self.assertNotEqual(prompt.get_choices(context, self.user, self.factory.get("/")), tuple())
del context["fixed_choice_prompt_expression"]
# Single choice
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.RADIO_BUTTON_GROUP,
placeholder="return prompt_context['foo']",
placeholder_expression=True,
)
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")), (context["foo"],)
)
context["fixed_choice_prompt_expression"] = generate_id()
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")),
context["fixed_choice_prompt_expression"],
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")),
(context["fixed_choice_prompt_expression"],),
)
self.assertNotEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
self.assertNotEqual(
prompt.get_choices(context, self.user, self.factory.get("/")), (context["foo"],)
)
del context["fixed_choice_prompt_expression"]
# Multi choice
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.DROPDOWN,
placeholder="return [prompt_context['foo'], True, 'text']",
placeholder_expression=True,
)
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")),
(context["foo"], True, "text"),
)
context["fixed_choice_prompt_expression"] = tuple(["text", generate_id(), 2])
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")),
"text",
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")),
context["fixed_choice_prompt_expression"],
)
self.assertNotEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
self.assertNotEqual(
prompt.get_choices(context, self.user, self.factory.get("/")),
(context["foo"], True, "text"),
)
def test_choices_are_none_for_non_choice_fields(self):
"""Test choices are None for non choice fields"""
context = {}
prompt: Prompt = Prompt(
field_key="text_prompt_expression",
label="TEXT_LABEL",
type=FieldTypes.TEXT,
placeholder="choice",
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")),
None,
)
def test_prompt_placeholder_error(self):
"""Test placeholder and expression"""
context = {}

View File

@ -24125,24 +24125,32 @@ paths:
- checkbox
- date
- date-time
- dropdown
- email
- file
- hidden
- number
- password
- radio-button-group
- separator
- static
- text
- text_area
- text_area_read_only
- text_read_only
- username
description: |-
* `text` - Text: Simple Text input
* `text_area` - Text area: Multiline Text Input.
* `text_read_only` - Text (read-only): Simple Text input, but cannot be edited.
* `text_area_read_only` - Text area (read-only): Multiline Text input, but cannot be edited.
* `username` - Username: Same as Text input, but checks for and prevents duplicate usernames.
* `email` - Email: Text field with Email type.
* `password` - Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical.
* `number` - Number
* `checkbox` - Checkbox
* `radio-button-group` - Fixed choice field rendered as a group of radio buttons.
* `dropdown` - Fixed choice field rendered as a dropdown.
* `date` - Date
* `date-time` - Date Time
* `file` - File: File upload for arbitrary files. File content will be available in flow context as data-URI
@ -24152,12 +24160,16 @@ paths:
* `ak-locale` - authentik: Selection of locales authentik supports
* `text` - Text: Simple Text input
* `text_area` - Text area: Multiline Text Input.
* `text_read_only` - Text (read-only): Simple Text input, but cannot be edited.
* `text_area_read_only` - Text area (read-only): Multiline Text input, but cannot be edited.
* `username` - Username: Same as Text input, but checks for and prevents duplicate usernames.
* `email` - Email: Text field with Email type.
* `password` - Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical.
* `number` - Number
* `checkbox` - Checkbox
* `radio-button-group` - Fixed choice field rendered as a group of radio buttons.
* `dropdown` - Fixed choice field rendered as a dropdown.
* `date` - Date
* `date-time` - Date Time
* `file` - File: File upload for arbitrary files. File content will be available in flow context as data-URI
@ -36262,6 +36274,8 @@ components:
type: boolean
placeholder:
type: string
description: When creating a Radio Button Group or Dropdown, enable interpreting
as expression and return a list to return multiple choices.
order:
type: integer
maximum: 2147483647
@ -37388,6 +37402,8 @@ components:
type: boolean
placeholder:
type: string
description: When creating a Radio Button Group or Dropdown, enable interpreting
as expression and return a list to return multiple choices.
order:
type: integer
maximum: 2147483647
@ -37461,6 +37477,8 @@ components:
type: boolean
placeholder:
type: string
description: When creating a Radio Button Group or Dropdown, enable interpreting
as expression and return a list to return multiple choices.
order:
type: integer
maximum: 2147483647
@ -37554,12 +37572,16 @@ components:
PromptTypeEnum:
enum:
- text
- text_area
- text_read_only
- text_area_read_only
- username
- email
- password
- number
- checkbox
- radio-button-group
- dropdown
- date
- date-time
- file
@ -37570,12 +37592,16 @@ components:
type: string
description: |-
* `text` - Text: Simple Text input
* `text_area` - Text area: Multiline Text Input.
* `text_read_only` - Text (read-only): Simple Text input, but cannot be edited.
* `text_area_read_only` - Text area (read-only): Multiline Text input, but cannot be edited.
* `username` - Username: Same as Text input, but checks for and prevents duplicate usernames.
* `email` - Email: Text field with Email type.
* `password` - Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical.
* `number` - Number
* `checkbox` - Checkbox
* `radio-button-group` - Fixed choice field rendered as a group of radio buttons.
* `dropdown` - Fixed choice field rendered as a dropdown.
* `date` - Date
* `date-time` - Date Time
* `file` - File: File upload for arbitrary files. File content will be available in flow context as data-URI
@ -39448,7 +39474,13 @@ components:
type: integer
sub_text:
type: string
choices:
type: array
items:
type: string
nullable: true
required:
- choices
- field_key
- label
- order

View File

@ -49,12 +49,24 @@ export class PromptForm extends ModelForm<Prompt, string> {
>
${t`Text: Simple Text input`}
</option>
<option
value=${PromptTypeEnum.TextArea}
?selected=${this.instance?.type === PromptTypeEnum.TextArea}
>
${t`Text Area: Multiline text input`}
</option>
<option
value=${PromptTypeEnum.TextReadOnly}
?selected=${this.instance?.type === PromptTypeEnum.TextReadOnly}
>
${t`Text (read-only): Simple Text input, but cannot be edited.`}
</option>
<option
value=${PromptTypeEnum.TextAreaReadOnly}
?selected=${this.instance?.type === PromptTypeEnum.TextAreaReadOnly}
>
${t`Text Area (read-only): Multiline text input, but cannot be edited.`}
</option>
<option
value=${PromptTypeEnum.Username}
?selected=${this.instance?.type === PromptTypeEnum.Username}
@ -85,6 +97,18 @@ export class PromptForm extends ModelForm<Prompt, string> {
>
${t`Checkbox`}
</option>
<option
value=${PromptTypeEnum.RadioButtonGroup}
?selected=${this.instance?.type === PromptTypeEnum.RadioButtonGroup}
>
${t`Radio Button Group (fixed choice)`}
</option>
<option
value=${PromptTypeEnum.Dropdown}
?selected=${this.instance?.type === PromptTypeEnum.Dropdown}
>
${t`Dropdown (fixed choice)`}
</option>
<option
value=${PromptTypeEnum.Date}
?selected=${this.instance?.type === PromptTypeEnum.Date}
@ -210,7 +234,11 @@ export class PromptForm extends ModelForm<Prompt, string> {
<ak-form-element-horizontal label=${t`Placeholder`} name="placeholder">
<ak-codemirror mode="python" value="${ifDefined(this.instance?.placeholder)}">
</ak-codemirror>
<p class="pf-c-form__helper-text">${t`Optionally pre-fill the input value`}</p>
<p class="pf-c-form__helper-text">
${t`Optionally pre-fill the input value.
When creating a "Radio Button Group" or "Dropdown", enable interpreting as
expression and return a list to return multiple choices.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Help text`} name="subText">
<ak-codemirror mode="htmlmixed" value="${ifDefined(this.instance?.subText)}">

View File

@ -6,7 +6,7 @@ import { BaseStage } from "@goauthentik/flow/stages/base";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
@ -28,7 +28,22 @@ import {
@customElement("ak-stage-prompt")
export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFAlert, PFForm, PFFormControl, PFTitle, PFButton];
return [
PFBase,
PFLogin,
PFAlert,
PFForm,
PFFormControl,
PFTitle,
PFButton,
css`
textarea {
min-height: 4em;
max-height: 15em;
resize: vertical;
}
`,
];
}
renderPromptInner(prompt: StagePrompt, placeholderAsValue: boolean): string {
@ -42,6 +57,15 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
class="pf-c-form-control"
?required=${prompt.required}
value="${placeholderAsValue ? prompt.placeholder : ""}">`;
case PromptTypeEnum.TextArea:
return `<textarea
type="text"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
autocomplete="off"
class="pf-c-form-control"
?required=${prompt.required}
value="${placeholderAsValue ? prompt.placeholder : ""}">`;
case PromptTypeEnum.TextReadOnly:
return `<input
type="text"
@ -49,6 +73,13 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
class="pf-c-form-control"
readonly
value="${prompt.placeholder}">`;
case PromptTypeEnum.TextAreaReadOnly:
return `<textarea
type="text"
name="${prompt.fieldKey}"
class="pf-c-form-control"
readonly
value="${prompt.placeholder}">`;
case PromptTypeEnum.Username:
return `<input
type="text"
@ -113,6 +144,38 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
?required=${prompt.required}>`;
case PromptTypeEnum.Static:
return `<p>${prompt.placeholder}</p>`;
case PromptTypeEnum.Dropdown:
return `<select class="pf-c-form-control" name="${prompt.fieldKey}">
${prompt.choices
?.map((choice) => {
return `<option
value=${choice}
${prompt.placeholder === choice ? "selected" : ""}
>
${choice}
</option>`;
})
.join("")}
</select>`;
case PromptTypeEnum.RadioButtonGroup:
return (
prompt.choices
?.map((choice) => {
return ` <div class="pf-c-check">
<input
type="radio"
class="pf-c-check__input"
name="${prompt.fieldKey}"
checked="${prompt.placeholder === choice}"
required="${prompt.required}"
value="${choice}"
/>
<label class="pf-c-check__label">${choice}</label>
</div>
`;
})
.join("") || ""
);
case PromptTypeEnum.AkLocale:
return `<select class="pf-c-form-control" name="${prompt.fieldKey}">
<option value="" ${prompt.placeholder === "" ? "selected" : ""}>

View File

@ -8,28 +8,37 @@ This stage is used to show the user arbitrary prompts.
The prompt can be any of the following types:
| Type | Description |
| ----------------- | ------------------------------------------------------------------------------------------ |
| Text | Arbitrary text. No client-side validation is done. |
| Text (Read only) | Same as above, but cannot be edited. |
| Username | Same as text, except the username is validated to be unique. |
| Email | Text input, ensures the value is an email address (validation is only done client-side). |
| Password | Same as text, shown as a password field client-side, and custom validation (see below). |
| Number | Numerical textbox. |
| Checkbox | Simple checkbox. |
| Date | Same as text, except the client renders a date-picker |
| Date-time | Same as text, except the client renders a date-time-picker |
| File | Allow users to upload a file, which will be available as base64-encoded data in the flow . |
| Separator | Passive element to group surrounding elements |
| Hidden | Hidden input field. Allows for the pre-setting of default values. |
| Static | Display arbitrary value as is |
| authentik: Locale | Display a list of all locales authentik supports. |
| Type | Description |
| --------------------- | ------------------------------------------------------------------------------------------ |
| Text | Arbitrary text. No client-side validation is done. |
| Text (Read only) | Same as above, but cannot be edited. |
| Text Area | Arbitrary multiline text. No client-side validation is done. |
| Text Area (Read only) | Same as above, but cannot be edited. |
| Username | Same as text, except the username is validated to be unique. |
| Email | Text input, ensures the value is an email address (validation is only done client-side). |
| Password | Same as text, shown as a password field client-side, and custom validation (see below). |
| Number | Numerical textbox. |
| Checkbox | Simple checkbox. |
| Radio Button Group | Similar to checkboxes, but allows selecting a value from a set of predefined values. |
| Dropdwon | A simple dropdown menu filled with predefined values. |
| Date | Same as text, except the client renders a date-picker |
| Date-time | Same as text, except the client renders a date-time-picker |
| File | Allow users to upload a file, which will be available as base64-encoded data in the flow . |
| Separator | Passive element to group surrounding elements |
| Hidden | Hidden input field. Allows for the pre-setting of default values. |
| Static | Display arbitrary value as is |
| authentik: Locale | Display a list of all locales authentik supports. |
:::note
`Radio Button Group` and `Dropdown` options require authentik 2023.3+
:::
Some types have special behaviors:
- _Username_: Input is validated against other usernames to ensure a unique value is provided.
- _Password_: All prompts with the type password within the same stage are compared and must be equal. If they are not equal, an error is shown
- _Hidden_ and _Static_: Their placeholder values are defaults and are not user-changeable.
- _Radio Button Group_ and _Dropdown_: Only allow the user to select one of a set of predefined values.
A prompt has the following attributes:
@ -56,6 +65,8 @@ A field placeholder, shown within the input field. This field is also used by th
By default, the placeholder is interpreted as-is. If you enable _Interpret placeholder as expression_, the placeholder
will be evaluated as a python expression. This happens in the same environment as [_Property mappings_](../../../property-mappings/expression).
In the case of `Radio Button Group` and `Dropdown` prompts, this field defines all possible values. When interpreted as-is, only one value will be allowed (the placeholder string). When interpreted as expression, a list of values can be returned to define multiple choices. For example, `return ["first option", 42, "another option"]` defines 3 possible values.
You can access both the HTTP request and the user as with a mapping. Additionally, you can access `prompt_context`, which is a dictionary of the current state of the prompt stage's data.
### `order`