Skip to content

Commit

Permalink
stages/prompt: add basic file field (#3156)
Browse files Browse the repository at this point in the history
add basic file field

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
  • Loading branch information
BeryJu committed Jul 5, 2022
1 parent 7133371 commit 49cce6a
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 20 deletions.
48 changes: 48 additions & 0 deletions authentik/stages/prompt/migrations/0008_alter_prompt_type.py
@@ -0,0 +1,48 @@
# Generated by Django 4.0.5 on 2022-06-26 21:27

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("authentik_stages_prompt", "0007_prompt_placeholder_expression"),
]

operations = [
migrations.AlterField(
model_name="prompt",
name="type",
field=models.CharField(
choices=[
("text", "Text: Simple Text input"),
(
"text_read_only",
"Text (read-only): Simple 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"),
("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,
),
),
]
30 changes: 29 additions & 1 deletion authentik/stages/prompt/models.py
@@ -1,11 +1,14 @@
"""prompt models"""
from base64 import b64decode
from typing import Any, Optional
from urllib.parse import urlparse
from uuid import uuid4

from django.db import models
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.exceptions import ValidationError
from rest_framework.fields import (
BooleanField,
CharField,
Expand All @@ -32,7 +35,7 @@
class FieldTypes(models.TextChoices):
"""Field types an Prompt can be"""

# update website/docs/flow/stages/prompt.index.md
# update website/docs/flow/stages/prompt/index.md

# Simple text field
TEXT = "text", _("Text: Simple Text input")
Expand Down Expand Up @@ -61,13 +64,36 @@ class FieldTypes(models.TextChoices):
DATE = "date"
DATE_TIME = "date-time"

FILE = (
"file",
_(
"File: File upload for arbitrary files. File content will be available in flow "
"context as data-URI"
),
)

SEPARATOR = "separator", _("Separator: Static Separator Line")
HIDDEN = "hidden", _("Hidden: Hidden field, can be used to insert data into form.")
STATIC = "static", _("Static: Static value, displayed as-is.")

AK_LOCALE = "ak-locale", _("authentik: Selection of locales authentik supports")


class InlineFileField(CharField):
"""Field for inline data-URI base64 encoded files"""

def to_internal_value(self, data: str):
uri = urlparse(data)
if uri.scheme != "data":
raise ValidationError("Invalid scheme")
header, encoded = uri.path.split(",", 1)
_mime, _, enc = header.partition(";")
if enc != "base64":
raise ValidationError("Invalid encoding")
data = b64decode(encoded.encode()).decode()
return super().to_internal_value(data)


class Prompt(SerializerModel):
"""Single Prompt, part of a prompt stage."""

Expand Down Expand Up @@ -134,6 +160,8 @@ def field(self, default: Optional[Any]) -> CharField:
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
Expand Down
15 changes: 13 additions & 2 deletions authentik/stages/prompt/tests.py
Expand Up @@ -3,7 +3,7 @@

from django.test import RequestFactory
from django.urls import reverse
from rest_framework.exceptions import ErrorDetail
from rest_framework.exceptions import ErrorDetail, ValidationError

from authentik.core.tests.utils import create_test_admin_user
from authentik.flows.markers import StageMarker
Expand All @@ -13,7 +13,7 @@
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.policies.expression.models import ExpressionPolicy
from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage
from authentik.stages.prompt.models import FieldTypes, InlineFileField, Prompt, PromptStage
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT, PromptChallengeResponse


Expand Down Expand Up @@ -110,6 +110,17 @@ def setUp(self):

self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)

def test_inline_file_field(self):
"""test InlineFileField"""
with self.assertRaises(ValidationError):
InlineFileField().to_internal_value("foo")
with self.assertRaises(ValidationError):
InlineFileField().to_internal_value("data:foo/bar;foo,qwer")
self.assertEqual(
InlineFileField().to_internal_value("data:mine/type;base64,Zm9v"),
"foo",
)

def test_render(self):
"""Test render of form, check if all prompts are rendered correctly"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
Expand Down
2 changes: 2 additions & 0 deletions schema.yml
Expand Up @@ -17741,6 +17741,7 @@ paths:
- date
- date-time
- email
- file
- hidden
- number
- password
Expand Down Expand Up @@ -29242,6 +29243,7 @@ components:
- checkbox
- date
- date-time
- file
- separator
- hidden
- static
Expand Down
20 changes: 19 additions & 1 deletion web/src/flows/stages/base.ts
Expand Up @@ -12,6 +12,17 @@ export interface StageHost {
readonly tenant: CurrentTenant;
}

export function readFileAsync(file: Blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}

export class BaseStage<Tin, Tout> extends LitElement {
host!: StageHost;

Expand All @@ -24,7 +35,14 @@ export class BaseStage<Tin, Tout> extends LitElement {
[key: string]: unknown;
} = {};
const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);
form.forEach((value, key) => (object[key] = value));

for await (const [key, value] of form.entries()) {
if (value instanceof Blob) {
object[key] = await readFileAsync(value);
} else {
object[key] = value;
}
}
return this.host?.submit(object as unknown as Tout).then((successful) => {
if (successful) {
this.cleanup();
Expand Down
9 changes: 8 additions & 1 deletion web/src/flows/stages/prompt/PromptStage.ts
Expand Up @@ -96,6 +96,13 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
?required=${prompt.required}>`;
case PromptTypeEnum.File:
return `<input
type="file"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
?required=${prompt.required}>`;
case PromptTypeEnum.Separator:
return `<ak-divider>${prompt.placeholder}</ak-divider>`;
case PromptTypeEnum.Hidden:
Expand Down Expand Up @@ -133,7 +140,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
return html`<p class="pf-c-form__helper-text">${unsafeHTML(prompt.subText)}</p>`;
}

shouldRenderInWrapper(prompt: StagePrompt): bool {
shouldRenderInWrapper(prompt: StagePrompt): boolean {
// Special types that aren't rendered in a wrapper
if (
prompt.type === PromptTypeEnum.Static ||
Expand Down
6 changes: 6 additions & 0 deletions web/src/pages/stages/prompt/PromptForm.ts
Expand Up @@ -97,6 +97,12 @@ export class PromptForm extends ModelForm<Prompt, string> {
>
${t`Date Time`}
</option>
<option
value=${PromptTypeEnum.File}
?selected=${this.instance?.type === PromptTypeEnum.File}
>
${t`File`}
</option>
<option
value=${PromptTypeEnum.Separator}
?selected=${this.instance?.type === PromptTypeEnum.Separator}
Expand Down
1 change: 1 addition & 0 deletions web/tsconfig.json
Expand Up @@ -24,6 +24,7 @@
"ES2020",
"ESNext",
"DOM",
"DOM.Iterable",
"WebWorker"
],
"plugins": [
Expand Down
31 changes: 16 additions & 15 deletions website/docs/flow/stages/prompt/index.md
Expand Up @@ -8,21 +8,22 @@ 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 |
| 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. |
| 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. |

Some types have special behaviors:

Expand Down

0 comments on commit 49cce6a

Please sign in to comment.