web/admin: migrate FlowStageBinding form to web

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-03-31 15:33:58 +02:00
parent 0395c84270
commit 25e043afea
8 changed files with 194 additions and 176 deletions

View File

@ -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,
)
)

View File

@ -9,7 +9,6 @@ from authentik.admin.views import (
providers, providers,
sources, sources,
stages, stages,
stages_bindings,
) )
from authentik.providers.saml.views.metadata import MetadataImportView from authentik.providers.saml.views.metadata import MetadataImportView
@ -62,17 +61,6 @@ urlpatterns = [
stages.StageUpdateView.as_view(), stages.StageUpdateView.as_view(),
name="stage-update", name="stage-update",
), ),
# Stage bindings
path(
"stages/bindings/create/",
stages_bindings.StageBindingCreateView.as_view(),
name="stage-binding-create",
),
path(
"stages/bindings/<uuid:pk>/update/",
stages_bindings.StageBindingUpdateView.as_view(),
name="stage-binding-update",
),
# Property Mappings # Property Mappings
path( path(
"property-mappings/create/", "property-mappings/create/",

View File

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

View File

@ -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(),
}

View File

@ -12,8 +12,8 @@ from rest_framework.fields import (
DateField, DateField,
DateTimeField, DateTimeField,
EmailField, EmailField,
HiddenField,
IntegerField, IntegerField,
HiddenField
) )
from rest_framework.serializers import BaseSerializer from rest_framework.serializers import BaseSerializer

View File

@ -24,10 +24,6 @@ export class AdminURLManager {
return `/administration/stages/${rest}`; return `/administration/stages/${rest}`;
} }
static stageBindings(rest: string): string {
return `/administration/stages/bindings/${rest}`;
}
static sources(rest: string): string { static sources(rest: string): string {
return `/administration/sources/${rest}`; return `/administration/sources/${rest}`;
} }

View File

@ -4,6 +4,8 @@ import { AKResponse } from "../../api/Client";
import { Table, TableColumn } from "../../elements/table/Table"; import { Table, TableColumn } from "../../elements/table/Table";
import "../../elements/forms/DeleteForm"; import "../../elements/forms/DeleteForm";
import "../../elements/forms/ModalForm";
import "./StageBindingForm";
import "../../elements/Tabs"; import "../../elements/Tabs";
import "../../elements/buttons/ModalButton"; import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton"; import "../../elements/buttons/SpinnerButton";
@ -14,6 +16,7 @@ import { PAGE_SIZE } from "../../constants";
import { FlowsApi, FlowStageBinding, StagesApi } from "authentik-api"; import { FlowsApi, FlowStageBinding, StagesApi } from "authentik-api";
import { DEFAULT_CONFIG } from "../../api/Config"; import { DEFAULT_CONFIG } from "../../api/Config";
import { AdminURLManager } from "../../api/legacy"; import { AdminURLManager } from "../../api/legacy";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-bound-stages-list") @customElement("ak-bound-stages-list")
export class BoundStagesList extends Table<FlowStageBinding> { export class BoundStagesList extends Table<FlowStageBinding> {
@ -52,12 +55,19 @@ export class BoundStagesList extends Table<FlowStageBinding> {
</ak-spinner-button> </ak-spinner-button>
<div slot="modal"></div> <div slot="modal"></div>
</ak-modal-button> </ak-modal-button>
<ak-modal-button href="${AdminURLManager.stageBindings(`${item.pk}/update/`)}"> <ak-forms-modal>
<ak-spinner-button slot="trigger" class="pf-m-secondary"> <span slot="submit">
${gettext("Update")}
</span>
<span slot="header">
${gettext("Update Stage binding")}
</span>
<ak-stage-binding-form slot="form" .fsb=${item}>
</ak-stage-binding-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${gettext("Edit Binding")} ${gettext("Edit Binding")}
</ak-spinner-button> </button>
<div slot="modal"></div> </ak-forms-modal>
</ak-modal-button>
<ak-forms-delete <ak-forms-delete
.obj=${item} .obj=${item}
objectLabel=${gettext("Stage binding")} objectLabel=${gettext("Stage binding")}
@ -95,12 +105,19 @@ export class BoundStagesList extends Table<FlowStageBinding> {
${gettext("No stages are currently bound to this flow.")} ${gettext("No stages are currently bound to this flow.")}
</div> </div>
<div slot="primary"> <div slot="primary">
<ak-modal-button href="${AdminURLManager.stageBindings(`create/?target=${this.target}`)}"> <ak-forms-modal>
<ak-spinner-button slot="trigger" class="pf-m-primary"> <span slot="submit">
${gettext("Bind Stage")} ${gettext("Create")}
</ak-spinner-button> </span>
<div slot="modal"></div> <span slot="header">
</ak-modal-button> ${gettext("Create Stage binding")}
</span>
<ak-stage-binding-form slot="form" targetPk=${ifDefined(this.target)}>
</ak-stage-binding-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${gettext("Bind stage")}
</button>
</ak-forms-modal>
</div> </div>
</ak-empty-state>`); </ak-empty-state>`);
} }
@ -127,12 +144,19 @@ export class BoundStagesList extends Table<FlowStageBinding> {
}), html`<ak-spinner></ak-spinner>`)} }), html`<ak-spinner></ak-spinner>`)}
</ul> </ul>
</ak-dropdown> </ak-dropdown>
<ak-modal-button href="${AdminURLManager.stageBindings(`create/?target=${this.target}`)}"> <ak-forms-modal>
<ak-spinner-button slot="trigger" class="pf-m-primary"> <span slot="submit">
${gettext("Bind Stage")} ${gettext("Create")}
</ak-spinner-button> </span>
<div slot="modal"></div> <span slot="header">
</ak-modal-button> ${gettext("Create Stage binding")}
</span>
<ak-stage-binding-form slot="form" targetPk=${ifDefined(this.target)}>
</ak-stage-binding-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${gettext("Bind stage")}
</button>
</ak-forms-modal>
${super.renderToolbar()} ${super.renderToolbar()}
`; `;
} }

View File

@ -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<FlowStageBinding> {
@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<FlowStageBinding> => {
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<string, Stage[]>();
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`<optgroup label=${group}>
${stages.map(stage => {
const selected = (this.fsb?.stage === stage.pk);
return html`<option ?selected=${selected} value=${ifDefined(stage.pk)}>${stage.name}</option>`;
})}
</optgroup>`;
})}
`;
}
getOrder(): Promise<number> {
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`
<input required name="target" type="hidden" value=${ifDefined(this.fsb?.target || this.targetPk)}>
`;
}
return html`<ak-form-element-horizontal
label=${gettext("Target")}
?required=${true}
name="target">
<select class="pf-c-form-control">
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
ordering: "pk"
}).then(flows => {
return flows.results.map(flow => {
// No ?selected check here, as this input isnt shown on update forms
return html`<option value=${ifDefined(flow.pk)}>${flow.name}</option>`;
});
}), html``)}
</select>
</ak-form-element-horizontal>`;
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
${this.renderTarget()}
<ak-form-element-horizontal
label=${gettext("Stage")}
?required=${true}
name="stage">
<select class="pf-c-form-control">
${until(new StagesApi(DEFAULT_CONFIG).stagesAllList({
ordering: "pk"
}).then(stages => {
return this.groupStages(stages.results);
}), html``)}
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="evaluateOnPlan">
<div class="pf-c-check">
<input type="checkbox" class="pf-c-check__input" ?checked=${this.fsb?.evaluateOnPlan || true}>
<label class="pf-c-check__label">
${gettext("Evaluate on plan")}
</label>
</div>
<p class="pf-c-form__helper-text">${gettext("Evaluate policies during the Flow planning process. Disable this for input-based policies.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="reEvaluatePolicies">
<div class="pf-c-check">
<input type="checkbox" class="pf-c-check__input" ?checked=${this.fsb?.reEvaluatePolicies || false}>
<label class="pf-c-check__label">
${gettext("Re-evaluate policies")}
</label>
</div>
<p class="pf-c-form__helper-text">${gettext("Evaluate policies when the Stage is present to the user.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${gettext("Order")}
?required=${true}
name="order">
<input type="text" value="${until(this.getOrder(), this.fsb?.order)}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${gettext("Policy engine mode")}
?required=${true}
name="policyEngineMode">
<select class="pf-c-form-control">
<option value=${FlowStageBindingPolicyEngineModeEnum.Any} ?selected=${this.fsb?.policyEngineMode === FlowStageBindingPolicyEngineModeEnum.Any}>
${gettext("ANY, any policy must match to grant access.")}
</option>
<option value=${FlowStageBindingPolicyEngineModeEnum.All} ?selected=${this.fsb?.policyEngineMode === FlowStageBindingPolicyEngineModeEnum.All}>
${gettext("ALL, all policies must match to grant access.")}
</option>
</select>
</ak-form-element-horizontal>
</form>`;
}
}