web/admin: migrate FlowStageBinding form to web
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
0395c84270
commit
25e043afea
|
@ -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,
|
||||
)
|
||||
)
|
|
@ -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/<uuid:pk>/update/",
|
||||
stages_bindings.StageBindingUpdateView.as_view(),
|
||||
name="stage-binding-update",
|
||||
),
|
||||
# Property Mappings
|
||||
path(
|
||||
"property-mappings/create/",
|
||||
|
|
|
@ -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")
|
|
@ -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(),
|
||||
}
|
|
@ -12,8 +12,8 @@ from rest_framework.fields import (
|
|||
DateField,
|
||||
DateTimeField,
|
||||
EmailField,
|
||||
HiddenField,
|
||||
IntegerField,
|
||||
HiddenField
|
||||
)
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
@ -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<FlowStageBinding> {
|
||||
|
@ -52,12 +55,19 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
|||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<ak-modal-button href="${AdminURLManager.stageBindings(`${item.pk}/update/`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
||||
<ak-forms-modal>
|
||||
<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")}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
<ak-forms-delete
|
||||
.obj=${item}
|
||||
objectLabel=${gettext("Stage binding")}
|
||||
|
@ -95,12 +105,19 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
|||
${gettext("No stages are currently bound to this flow.")}
|
||||
</div>
|
||||
<div slot="primary">
|
||||
<ak-modal-button href="${AdminURLManager.stageBindings(`create/?target=${this.target}`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
||||
${gettext("Bind Stage")}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">
|
||||
${gettext("Create")}
|
||||
</span>
|
||||
<span slot="header">
|
||||
${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>
|
||||
</ak-empty-state>`);
|
||||
}
|
||||
|
@ -127,12 +144,19 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
|||
}), html`<ak-spinner></ak-spinner>`)}
|
||||
</ul>
|
||||
</ak-dropdown>
|
||||
<ak-modal-button href="${AdminURLManager.stageBindings(`create/?target=${this.target}`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
||||
${gettext("Bind Stage")}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">
|
||||
${gettext("Create")}
|
||||
</span>
|
||||
<span slot="header">
|
||||
${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()}
|
||||
`;
|
||||
}
|
||||
|
|
|
@ -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>`;
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue