From 25e043afea001941bf5d8a1f7cf8ca61e6922b0c Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 31 Mar 2021 15:33:58 +0200 Subject: [PATCH] web/admin: migrate FlowStageBinding form to web Signed-off-by: Jens Langhammer --- authentik/admin/tests/test_stage_bindings.py | 43 ------ authentik/admin/urls.py | 12 -- authentik/admin/views/stages_bindings.py | 65 -------- authentik/flows/forms.py | 34 ----- authentik/stages/prompt/models.py | 2 +- web/src/api/legacy.ts | 4 - web/src/pages/flows/BoundStagesList.ts | 58 ++++--- web/src/pages/flows/StageBindingForm.ts | 152 +++++++++++++++++++ 8 files changed, 194 insertions(+), 176 deletions(-) delete mode 100644 authentik/admin/tests/test_stage_bindings.py delete mode 100644 authentik/admin/views/stages_bindings.py delete mode 100644 authentik/flows/forms.py create mode 100644 web/src/pages/flows/StageBindingForm.ts diff --git a/authentik/admin/tests/test_stage_bindings.py b/authentik/admin/tests/test_stage_bindings.py deleted file mode 100644 index 481fcaca4..000000000 --- a/authentik/admin/tests/test_stage_bindings.py +++ /dev/null @@ -1,43 +0,0 @@ -"""admin tests""" -from uuid import uuid4 - -from django import forms -from django.test import TestCase -from django.test.client import RequestFactory - -from authentik.admin.views.stages_bindings import StageBindingCreateView -from authentik.flows.forms import FlowStageBindingForm -from authentik.flows.models import Flow - - -class TestStageBindingView(TestCase): - """Generic admin tests""" - - def setUp(self): - self.factory = RequestFactory() - - def test_without_get_param(self): - """Test StageBindingCreateView without get params""" - request = self.factory.get("/") - view = StageBindingCreateView(request=request) - self.assertEqual(view.get_initial(), {}) - - def test_with_params_invalid(self): - """Test StageBindingCreateView with invalid get params""" - request = self.factory.get("/", {"target": uuid4()}) - view = StageBindingCreateView(request=request) - self.assertEqual(view.get_initial(), {}) - - def test_with_params(self): - """Test StageBindingCreateView with get params""" - target = Flow.objects.create(name="test", slug="test") - request = self.factory.get("/", {"target": target.pk.hex}) - view = StageBindingCreateView(request=request) - self.assertEqual(view.get_initial(), {"target": target, "order": 0}) - - self.assertTrue( - isinstance( - FlowStageBindingForm(initial={"target": "foo"}).fields["target"].widget, - forms.HiddenInput, - ) - ) diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index aabbbe09b..0135dde93 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -9,7 +9,6 @@ from authentik.admin.views import ( providers, sources, stages, - stages_bindings, ) from authentik.providers.saml.views.metadata import MetadataImportView @@ -62,17 +61,6 @@ urlpatterns = [ stages.StageUpdateView.as_view(), name="stage-update", ), - # Stage bindings - path( - "stages/bindings/create/", - stages_bindings.StageBindingCreateView.as_view(), - name="stage-binding-create", - ), - path( - "stages/bindings//update/", - stages_bindings.StageBindingUpdateView.as_view(), - name="stage-binding-update", - ), # Property Mappings path( "property-mappings/create/", diff --git a/authentik/admin/views/stages_bindings.py b/authentik/admin/views/stages_bindings.py deleted file mode 100644 index 3bebcb75c..000000000 --- a/authentik/admin/views/stages_bindings.py +++ /dev/null @@ -1,65 +0,0 @@ -"""authentik StageBinding administration""" -from typing import Any - -from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.mixins import ( - PermissionRequiredMixin as DjangoPermissionRequiredMixin, -) -from django.contrib.messages.views import SuccessMessageMixin -from django.db.models import Max -from django.urls import reverse_lazy -from django.utils.translation import gettext as _ -from django.views.generic import UpdateView -from guardian.mixins import PermissionRequiredMixin - -from authentik.flows.forms import FlowStageBindingForm -from authentik.flows.models import Flow, FlowStageBinding -from authentik.lib.views import CreateAssignPermView - - -class StageBindingCreateView( - SuccessMessageMixin, - LoginRequiredMixin, - DjangoPermissionRequiredMixin, - CreateAssignPermView, -): - """Create new StageBinding""" - - model = FlowStageBinding - permission_required = "authentik_flows.add_flowstagebinding" - form_class = FlowStageBindingForm - - template_name = "generic/create.html" - success_url = reverse_lazy("authentik_core:if-admin") - success_message = _("Successfully created StageBinding") - - def get_initial(self) -> dict[str, Any]: - if "target" in self.request.GET: - initial_target_pk = self.request.GET["target"] - targets = Flow.objects.filter(pk=initial_target_pk).select_subclasses() - if not targets.exists(): - return {} - max_order = FlowStageBinding.objects.filter( - target=targets.first() - ).aggregate(Max("order"))["order__max"] - if not isinstance(max_order, int): - max_order = -1 - return {"target": targets.first(), "order": max_order + 1} - return super().get_initial() - - -class StageBindingUpdateView( - SuccessMessageMixin, - LoginRequiredMixin, - PermissionRequiredMixin, - UpdateView, -): - """Update FlowStageBinding""" - - model = FlowStageBinding - permission_required = "authentik_flows.change_flowstagebinding" - form_class = FlowStageBindingForm - - template_name = "generic/update.html" - success_url = reverse_lazy("authentik_core:if-admin") - success_message = _("Successfully updated StageBinding") diff --git a/authentik/flows/forms.py b/authentik/flows/forms.py deleted file mode 100644 index 79b4a2b31..000000000 --- a/authentik/flows/forms.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Flow and Stage forms""" -from django import forms - -from authentik.flows.models import FlowStageBinding, Stage -from authentik.lib.widgets import GroupedModelChoiceField - - -class FlowStageBindingForm(forms.ModelForm): - """FlowStageBinding Form""" - - stage = GroupedModelChoiceField( - queryset=Stage.objects.all().order_by("name").select_subclasses(), - to_field_name="stage_uuid", - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if "target" in self.initial: - self.fields["target"].widget = forms.HiddenInput() - - class Meta: - - model = FlowStageBinding - fields = [ - "target", - "stage", - "evaluate_on_plan", - "re_evaluate_policies", - "order", - "policy_engine_mode", - ] - widgets = { - "name": forms.TextInput(), - } diff --git a/authentik/stages/prompt/models.py b/authentik/stages/prompt/models.py index 9ebaf4bb3..9fa5644d9 100644 --- a/authentik/stages/prompt/models.py +++ b/authentik/stages/prompt/models.py @@ -12,8 +12,8 @@ from rest_framework.fields import ( DateField, DateTimeField, EmailField, + HiddenField, IntegerField, - HiddenField ) from rest_framework.serializers import BaseSerializer diff --git a/web/src/api/legacy.ts b/web/src/api/legacy.ts index 3a6c8cb3b..3692be40e 100644 --- a/web/src/api/legacy.ts +++ b/web/src/api/legacy.ts @@ -24,10 +24,6 @@ export class AdminURLManager { return `/administration/stages/${rest}`; } - static stageBindings(rest: string): string { - return `/administration/stages/bindings/${rest}`; - } - static sources(rest: string): string { return `/administration/sources/${rest}`; } diff --git a/web/src/pages/flows/BoundStagesList.ts b/web/src/pages/flows/BoundStagesList.ts index b37a6196a..93a4a89e2 100644 --- a/web/src/pages/flows/BoundStagesList.ts +++ b/web/src/pages/flows/BoundStagesList.ts @@ -4,6 +4,8 @@ import { AKResponse } from "../../api/Client"; import { Table, TableColumn } from "../../elements/table/Table"; import "../../elements/forms/DeleteForm"; +import "../../elements/forms/ModalForm"; +import "./StageBindingForm"; import "../../elements/Tabs"; import "../../elements/buttons/ModalButton"; import "../../elements/buttons/SpinnerButton"; @@ -14,6 +16,7 @@ import { PAGE_SIZE } from "../../constants"; import { FlowsApi, FlowStageBinding, StagesApi } from "authentik-api"; import { DEFAULT_CONFIG } from "../../api/Config"; import { AdminURLManager } from "../../api/legacy"; +import { ifDefined } from "lit-html/directives/if-defined"; @customElement("ak-bound-stages-list") export class BoundStagesList extends Table { @@ -52,12 +55,19 @@ export class BoundStagesList extends Table {
- - + + + ${gettext("Update")} + + + ${gettext("Update Stage binding")} + + + + + { ${gettext("No stages are currently bound to this flow.")}
- - - ${gettext("Bind Stage")} - -
-
+ + + ${gettext("Create")} + + + ${gettext("Create Stage binding")} + + + + +
`); } @@ -127,12 +144,19 @@ export class BoundStagesList extends Table { }), html``)} - - - ${gettext("Bind Stage")} - -
-
+ + + ${gettext("Create")} + + + ${gettext("Create Stage binding")} + + + + + ${super.renderToolbar()} `; } diff --git a/web/src/pages/flows/StageBindingForm.ts b/web/src/pages/flows/StageBindingForm.ts new file mode 100644 index 000000000..e71828ce7 --- /dev/null +++ b/web/src/pages/flows/StageBindingForm.ts @@ -0,0 +1,152 @@ +import { FlowsApi, FlowStageBinding, FlowStageBindingPolicyEngineModeEnum, Stage, StagesApi } from "authentik-api"; +import { gettext } from "django"; +import { customElement, property } from "lit-element"; +import { html, TemplateResult } from "lit-html"; +import { DEFAULT_CONFIG } from "../../api/Config"; +import { Form } from "../../elements/forms/Form"; +import { until } from "lit-html/directives/until"; +import { ifDefined } from "lit-html/directives/if-defined"; +import "../../elements/forms/HorizontalFormElement"; + +@customElement("ak-stage-binding-form") +export class StageBindingForm extends Form { + + @property({attribute: false}) + fsb?: FlowStageBinding; + + @property() + targetPk?: string; + + getSuccessMessage(): string { + if (this.fsb) { + return gettext("Successfully updated binding."); + } else { + return gettext("Successfully created binding."); + } + } + + send = (data: FlowStageBinding): Promise => { + if (this.fsb) { + return new FlowsApi(DEFAULT_CONFIG).flowsBindingsUpdate({ + fsbUuid: this.fsb.pk || "", + data: data + }); + } else { + return new FlowsApi(DEFAULT_CONFIG).flowsBindingsCreate({ + data: data + }); + } + }; + + groupStages(stages: Stage[]): TemplateResult { + const m = new Map(); + stages.forEach(p => { + if (!m.has(p.verboseName || "")) { + m.set(p.verboseName || "", []); + } + const tProviders = m.get(p.verboseName || "") || []; + tProviders.push(p); + }); + return html` + ${Array.from(m).map(([group, stages]) => { + return html` + ${stages.map(stage => { + const selected = (this.fsb?.stage === stage.pk); + return html``; + })} + `; + })} + `; + } + + getOrder(): Promise { + if (this.fsb) { + return Promise.resolve(this.fsb.order); + } + return new FlowsApi(DEFAULT_CONFIG).flowsBindingsList({ + target: this.targetPk || "", + }).then(bindings => { + const orders = bindings.results.map(binding => binding.order); + return Math.max(...orders) + 1; + }); + } + + renderTarget(): TemplateResult { + if (this.fsb?.target || this.targetPk) { + return html` + + `; + } + return html` + + `; + } + + renderForm(): TemplateResult { + return html`
+ ${this.renderTarget()} + + + + +
+ + +
+

${gettext("Evaluate policies during the Flow planning process. Disable this for input-based policies.")}

+
+ +
+ + +
+

${gettext("Evaluate policies when the Stage is present to the user.")}

+
+ + + + + + +
`; + } + +}