admin: finalise migration

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-04-03 01:20:20 +02:00
parent d7698343ae
commit aaebd01058
16 changed files with 63 additions and 369 deletions

View File

@ -7,5 +7,4 @@ class AuthentikAdminConfig(AppConfig):
name = "authentik.admin" name = "authentik.admin"
label = "authentik_admin" label = "authentik_admin"
mountpoint = "administration/"
verbose_name = "authentik Admin" verbose_name = "authentik Admin"

View File

@ -1,18 +0,0 @@
{% extends base_template|default:"generic/form.html" %}
{% load authentik_utils %}
{% load i18n %}
{% block above_form %}
<h1>
{% blocktrans with type=form|form_verbose_name %}
Create {{ type }}
{% endblocktrans %}
</h1>
{% endblock %}
{% block action %}
{% blocktrans with type=form|form_verbose_name %}
Create {{ type }}
{% endblocktrans %}
{% endblock %}

View File

@ -1,38 +0,0 @@
{% load i18n %}
{% load authentik_utils %}
{% load static %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
{% block above_form %}
{% endblock %}
</div>
</section>
<section class="pf-c-page__main-section">
<div class="pf-l-stack">
<div class="pf-l-stack__item">
<div class="pf-c-card">
<div class="pf-c-card__body">
<form id="main-form" action="" method="post" class="pf-c-form pf-m-horizontal" enctype="multipart/form-data">
{% include 'partials/form_horizontal.html' with form=form %}
{% block beneath_form %}
{% endblock %}
</form>
</div>
</div>
</div>
</div>
</section>
<footer class="pf-c-modal-box__footer">
<ak-spinner-button form="main-form">
{% block action %}{% endblock %}
</ak-spinner-button>&nbsp;
<a class="pf-c-button pf-m-secondary" href="#/">{% trans "Cancel" %}</a>
</footer>
{% endblock %}
{% block scripts %}
{{ block.super }}
{{ form.media.js }}
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends base_template|default:"generic/form.html" %}
{% load authentik_utils %}
{% load i18n %}
{% block above_form %}
<h1>
{% blocktrans with type=form|form_verbose_name|title inst=form.instance %}
Update {{ inst }}
{% endblocktrans %}
</h1>
{% endblock %}
{% block action %}
{% blocktrans with type=form|form_verbose_name %}
Update {{ type }}
{% endblocktrans %}
{% endblock %}

View File

@ -1,14 +0,0 @@
"""authentik URL Configuration"""
from django.urls import path
from authentik.admin.views import stages
urlpatterns = [
# Stages
path("stages/create/", stages.StageCreateView.as_view(), name="stage-create"),
path(
"stages/<uuid:pk>/update/",
stages.StageUpdateView.as_view(),
name="stage-update",
),
]

View File

@ -1,43 +0,0 @@
"""authentik Stage administration"""
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.urls import reverse_lazy
from django.utils.translation import gettext as _
from guardian.mixins import PermissionRequiredMixin
from authentik.admin.views.utils import InheritanceCreateView, InheritanceUpdateView
from authentik.flows.models import Stage
class StageCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
InheritanceCreateView,
):
"""Create new Stage"""
model = Stage
template_name = "generic/create.html"
permission_required = "authentik_flows.add_stage"
success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully created Stage")
class StageUpdateView(
SuccessMessageMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,
):
"""Update stage"""
model = Stage
permission_required = "authentik_flows.update_application"
template_name = "generic/update.html"
success_url = reverse_lazy("authentik_core:if-admin")
success_message = _("Successfully updated Stage")

View File

@ -1,50 +0,0 @@
"""authentik admin util views"""
from typing import Any
from django.http import Http404
from django.views.generic import UpdateView
from authentik.lib.utils.reflection import all_subclasses
from authentik.lib.views import CreateAssignPermView
class InheritanceCreateView(CreateAssignPermView):
"""CreateView for objects using InheritanceManager"""
def get_form_class(self):
provider_type = self.request.GET.get("type")
try:
model = next(
x for x in all_subclasses(self.model) if x.__name__ == provider_type
)
except StopIteration as exc:
raise Http404 from exc
return model().form
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
class InheritanceUpdateView(UpdateView):
"""UpdateView for objects using InheritanceManager"""
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
def get_form_class(self):
return self.get_object().form
def get_object(self, queryset=None):
return (
self.model.objects.filter(pk=self.kwargs.get("pk"))
.select_subclasses()
.first()
)

View File

@ -1,115 +0,0 @@
{% load authentik_utils %}
{% load i18n %}
{% csrf_token %}
{% for field in form %}
{% if field.field.widget|fieldtype == 'HiddenInput' %}
{{ field }}
{% else %}
<div class="pf-c-form__group {% if field.errors %} has-error {% endif %}">
{% if field.field.widget|fieldtype == 'RadioSelect' %}
<div class="pf-c-form__group-label">
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
<span class="pf-c-form__label-text">{{ field.label }}</span>
{% if field.field.required %}
<span class="pf-c-form__label-required" aria-hidden="true">&#42;</span>
{% endif %}
</label>
</div>
<div class="pf-c-form__group-control">
{% for c in field %}
<div class="pf-c-radio">
<input class="pf-c-radio__input"
type="radio" id="{{ field.name }}-{{ forloop.counter0 }}"
name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}"
value="{{ c.data.value }}"
{% if c.data.selected %} checked {% endif %}/>
<label class="pf-c-radio__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
</div>
{% endfor %}
{% if field.help_text %}
<p class="pf-c-form__helper-text">{{ field.help_text }}</p>
{% endif %}
</div>
{% elif field.field.widget|fieldtype == 'Select' or field.field.widget|fieldtype == "SelectMultiple" %}
<div class="pf-c-form__group-label">
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
<span class="pf-c-form__label-text">{{ field.label }}</span>
{% if field.field.required %}
<span class="pf-c-form__label-required" aria-hidden="true">&#42;</span>
{% endif %}
</label>
</div>
<div class="pf-c-form__group-control">
<div class="pf-c-form__horizontal-group">
{{ field|css_class:"pf-c-form-control" }}
{% if field.help_text %}
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
{% endif %}
{% if field.field.widget|fieldtype == 'SelectMultiple' %}
<p class="pf-c-form__helper-text">{% trans 'Hold control/command to select multiple items.' %}</p>
{% endif %}
</div>
</div>
{% elif field.field.widget|fieldtype == 'CheckboxInput' %}
<div class="pf-c-form__group-control">
<div class="pf-c-form__horizontal-group">
<div class="pf-c-check">
{{ field|css_class:"pf-c-check__input" }}
<label class="pf-c-check__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ field.label }}</label>
</div>
{% if field.help_text %}
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
{% endif %}
</div>
</div>
{% elif field.field.widget|fieldtype == "FileInput" %}
<div class="pf-c-form__group-label">
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
<span class="pf-c-form__label-text">{{ field.label }}</span>
{% if field.field.required %}
<span class="pf-c-form__label-required" aria-hidden="true">&#42;</span>
{% endif %}
</label>
</div>
<div class="pf-c-form__group-control">
<div class="c-form__horizontal-group">
{{ field|css_class:"pf-c-form-control" }}
{% if field.help_text %}
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
{% endif %}
{% if field.value %}
<a target="_blank" href="{{ field.value.url }}" class="pf-c-form__helper-text">
{% blocktrans with current=field.value %}
Currently set to {{current}}.
{% endblocktrans %}
</a>
{% endif %}
</div>
</div>
{% else %}
<div class="pf-c-form__group-label">
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
<span class="pf-c-form__label-text">{{ field.label }}</span>
{% if field.field.required %}
<span class="pf-c-form__label-required" aria-hidden="true">&#42;</span>
{% endif %}
</label>
</div>
<div class="pf-c-form__group-control">
<div class="c-form__horizontal-group">
{{ field|css_class:'pf-c-form-control' }}
{% if field.help_text %}
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
{% endif %}
</div>
</div>
{% endif %}
{% for error in field.errors %}
<p class="pf-c-form__helper-text pf-m-error">
{{ error }}
</p>
{% endfor %}
</div>
{% endif %}
{% endfor %}

View File

@ -1,7 +1,6 @@
"""Flow Stage API Views""" """Flow Stage API Views"""
from typing import Iterable from typing import Iterable
from django.urls import reverse
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework import mixins from rest_framework import mixins
from rest_framework.decorators import action from rest_framework.decorators import action
@ -70,8 +69,7 @@ class StageViewSet(
{ {
"name": verbose_name(subclass), "name": verbose_name(subclass),
"description": subclass.__doc__, "description": subclass.__doc__,
"component": reverse("authentik_admin:stage-create") "component": subclass().component,
+ f"?type={subclass.__name__}",
} }
) )
data = sorted(data, key=lambda x: x["name"]) data = sorted(data, key=lambda x: x["name"])

View File

@ -3,7 +3,6 @@ from typing import TYPE_CHECKING, Optional, Type
from uuid import uuid4 from uuid import uuid4
from django.db import models from django.db import models
from django.forms import ModelForm
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
@ -60,8 +59,8 @@ class Stage(SerializerModel):
raise NotImplementedError raise NotImplementedError
@property @property
def form(self) -> Type[ModelForm]: def component(self) -> str:
"""Return Form class used to edit this object""" """Return component used to edit this object"""
raise NotImplementedError raise NotImplementedError
@property @property

View File

@ -1,31 +0,0 @@
"""flow model tests"""
from typing import Callable, Type
from django.forms import ModelForm
from django.test import TestCase
from authentik.flows.models import Stage
from authentik.flows.stage import StageView
class TestStageProperties(TestCase):
"""Generic model properties tests"""
def stage_tester_factory(model: Type[Stage]) -> Callable:
"""Test a form"""
def tester(self: TestStageProperties):
model_inst = model()
self.assertTrue(issubclass(model_inst.form, ModelForm))
self.assertTrue(issubclass(model_inst.type, StageView))
return tester
for stage_type in Stage.__subclasses__():
setattr(
TestStageProperties,
f"test_stage_{stage_type.__name__}",
stage_tester_factory(stage_type),
)

View File

@ -9,7 +9,6 @@ from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import FlowPlan from authentik.flows.planner import FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN from authentik.flows.views import SESSION_KEY_PLAN
from authentik.stages.deny.forms import DenyStageForm
from authentik.stages.deny.models import DenyStage from authentik.stages.deny.models import DenyStage
@ -52,8 +51,3 @@ class TestUserDenyStage(TestCase):
"type": ChallengeTypes.NATIVE.value, "type": ChallengeTypes.NATIVE.value,
}, },
) )
def test_form(self):
"""Test Form"""
data = {"name": "test"}
self.assertEqual(DenyStageForm(data).is_valid(), True)

View File

@ -5,7 +5,6 @@ from django.utils.encoding import force_str
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.stages.dummy.forms import DummyStageForm
from authentik.stages.dummy.models import DummyStage from authentik.stages.dummy.models import DummyStage
@ -49,8 +48,3 @@ class TestDummyStage(TestCase):
force_str(response.content), force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"}, {"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
) )
def test_form(self):
"""Test Form"""
data = {"name": "test"}
self.assertEqual(DummyStageForm(data).is_valid(), True)

View File

@ -1,15 +1,3 @@
export class AdminURLManager {
static policies(rest: string): string {
return `/administration/policies/${rest}`;
}
static stages(rest: string): string {
return `/administration/stages/${rest}`;
}
}
export class AppURLManager { export class AppURLManager {
static sourceSAML(slug: string, rest: string): string { static sourceSAML(slug: string, rest: string): string {

View File

@ -4,15 +4,34 @@ import { AKResponse } from "../../api/Client";
import { TableColumn } from "../../elements/table/Table"; import { TableColumn } from "../../elements/table/Table";
import { TablePage } from "../../elements/table/TablePage"; import { TablePage } from "../../elements/table/TablePage";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton"; import "../../elements/buttons/SpinnerButton";
import "../../elements/buttons/Dropdown"; import "../../elements/buttons/Dropdown";
import "../../elements/forms/DeleteForm"; import "../../elements/forms/DeleteForm";
import "../../elements/forms/ProxyForm";
import "../../elements/forms/ModalForm";
import { until } from "lit-html/directives/until"; import { until } from "lit-html/directives/until";
import { PAGE_SIZE } from "../../constants"; import { PAGE_SIZE } from "../../constants";
import { Stage, StagesApi } from "authentik-api"; import { Stage, StagesApi } from "authentik-api";
import { DEFAULT_CONFIG } from "../../api/Config"; import { DEFAULT_CONFIG } from "../../api/Config";
import { AdminURLManager } from "../../api/legacy"; import { ifDefined } from "lit-html/directives/if-defined";
import "./pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts";
import "./pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts";
import "./pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts";
import "./pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts";
import "./pages/stages/captcha/CaptchaStageForm.ts";
import "./pages/stages/consent/ConsentStageForm.ts";
import "./pages/stages/deny/DenyStageForm.ts";
import "./pages/stages/dummy/DummyStageForm.ts";
import "./pages/stages/email/EmailStageForm.ts";
import "./pages/stages/identification/IdentificationStageForm.ts";
import "./pages/stages/invitation/InvitationStageForm.ts";
import "./pages/stages/password/PasswordStageForm.ts";
import "./pages/stages/prompt/PromptStageForm.ts";
import "./pages/stages/user_delete/UserDeleteStageForm.ts";
import "./pages/stages/user_login/UserLoginStageForm.ts";
import "./pages/stages/user_logout/UserLogoutStageForm.ts";
import "./pages/stages/user_write/UserWriteStageForm.ts";
@customElement("ak-stage-list") @customElement("ak-stage-list")
export class StageListPage extends TablePage<Stage> { export class StageListPage extends TablePage<Stage> {
@ -61,12 +80,33 @@ export class StageListPage extends TablePage<Stage> {
</a>`; </a>`;
})}`, })}`,
html` html`
<ak-modal-button href="${AdminURLManager.stages(`${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 ${item.verboseName}`)}
</span>
<ak-proxy-form
slot="form"
.args=${{
"stageUUID": item.pk
}}
type=${ifDefined(item.objectType)}
.typeMap=${{
"dummy": "ak-policy-dummy-form",
"eventmatcher": "ak-policy-event-matcher-form",
"expression": "ak-policy-expression-form",
"passwordexpiry": "ak-policy-password-expiry-form",
"haveibeenpwend": "ak-policy-hibp-form",
"password": "ak-policy-password-form",
"reputation": "ak-policy-reputation-form",
}}>
</ak-proxy-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${gettext("Edit")} ${gettext("Edit")}
</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("Group")} objectLabel=${gettext("Group")}
@ -93,12 +133,21 @@ export class StageListPage extends TablePage<Stage> {
${until(new StagesApi(DEFAULT_CONFIG).stagesAllTypes().then((types) => { ${until(new StagesApi(DEFAULT_CONFIG).stagesAllTypes().then((types) => {
return types.map((type) => { return types.map((type) => {
return html`<li> return html`<li>
<ak-modal-button href="${type.component}"> <ak-forms-modal>
<button slot="trigger" class="pf-c-dropdown__menu-item">${type.name}<br> <span slot="submit">
${gettext("Create")}
</span>
<span slot="header">
${gettext(`Create ${type.name}`)}
</span>
<ak-proxy-form
slot="form"
type=${type.component}>
</ak-proxy-form>
<button slot="trigger" class="pf-c-dropdown__menu-item">
${type.name}<br>
<small>${type.description}</small> <small>${type.description}</small>
</button> </button>
<div slot="modal"></div>
</ak-modal-button>
</li>`; </li>`;
}); });
}), html`<ak-spinner></ak-spinner>`)} }), html`<ak-spinner></ak-spinner>`)}