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,
|
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/",
|
||||||
|
|
|
@ -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,
|
DateField,
|
||||||
DateTimeField,
|
DateTimeField,
|
||||||
EmailField,
|
EmailField,
|
||||||
|
HiddenField,
|
||||||
IntegerField,
|
IntegerField,
|
||||||
HiddenField
|
|
||||||
)
|
)
|
||||||
from rest_framework.serializers import BaseSerializer
|
from rest_framework.serializers import BaseSerializer
|
||||||
|
|
||||||
|
|
|
@ -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}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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