From 656fe00302c5bc10cc1041ed5798a486dafd7e32 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 31 Mar 2021 22:40:48 +0200 Subject: [PATCH] outposts: migrate service connections to web Signed-off-by: Jens Langhammer --- authentik/admin/urls.py | 12 -- .../views/outposts_service_connections.py | 44 ------- .../api/outpost_service_connections.py | 28 ++++- authentik/outposts/forms.py | 75 ------------ authentik/outposts/models.py | 19 ++- web/src/api/legacy.ts | 4 - .../outposts/ServiceConnectionDockerForm.ts | 111 ++++++++++++++++++ .../ServiceConnectionKubernetesForm.ts | 73 ++++++++++++ ...stPage.ts => ServiceConnectionListPage.ts} | 50 ++++++-- web/src/routes.ts | 2 +- 10 files changed, 256 insertions(+), 162 deletions(-) delete mode 100644 authentik/admin/views/outposts_service_connections.py delete mode 100644 authentik/outposts/forms.py create mode 100644 web/src/pages/outposts/ServiceConnectionDockerForm.ts create mode 100644 web/src/pages/outposts/ServiceConnectionKubernetesForm.ts rename web/src/pages/outposts/{OutpostServiceConnectionListPage.ts => ServiceConnectionListPage.ts} (70%) diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index 73615509d..d1b8dc550 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -2,7 +2,6 @@ from django.urls import path from authentik.admin.views import ( - outposts_service_connections, policies, property_mappings, providers, @@ -60,15 +59,4 @@ urlpatterns = [ property_mappings.PropertyMappingUpdateView.as_view(), name="property-mapping-update", ), - # Outpost Service Connections - path( - "outpost_service_connections/create/", - outposts_service_connections.OutpostServiceConnectionCreateView.as_view(), - name="outpost-service-connection-create", - ), - path( - "outpost_service_connections//update/", - outposts_service_connections.OutpostServiceConnectionUpdateView.as_view(), - name="outpost-service-connection-update", - ), ] diff --git a/authentik/admin/views/outposts_service_connections.py b/authentik/admin/views/outposts_service_connections.py deleted file mode 100644 index 9ffc5db9e..000000000 --- a/authentik/admin/views/outposts_service_connections.py +++ /dev/null @@ -1,44 +0,0 @@ -"""authentik OutpostServiceConnection 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.outposts.models import OutpostServiceConnection - - -class OutpostServiceConnectionCreateView( - SuccessMessageMixin, - LoginRequiredMixin, - DjangoPermissionRequiredMixin, - InheritanceCreateView, -): - """Create new OutpostServiceConnection""" - - model = OutpostServiceConnection - permission_required = "authentik_outposts.add_outpostserviceconnection" - - template_name = "generic/create.html" - success_url = reverse_lazy("authentik_core:if-admin") - success_message = _("Successfully created Outpost Service Connection") - - -class OutpostServiceConnectionUpdateView( - SuccessMessageMixin, - LoginRequiredMixin, - PermissionRequiredMixin, - InheritanceUpdateView, -): - """Update outpostserviceconnection""" - - model = OutpostServiceConnection - permission_required = "authentik_outposts.change_outpostserviceconnection" - - template_name = "generic/update.html" - success_url = reverse_lazy("authentik_core:if-admin") - success_message = _("Successfully updated Outpost Service Connection") diff --git a/authentik/outposts/api/outpost_service_connections.py b/authentik/outposts/api/outpost_service_connections.py index fc545e0fa..e5f042967 100644 --- a/authentik/outposts/api/outpost_service_connections.py +++ b/authentik/outposts/api/outpost_service_connections.py @@ -1,9 +1,12 @@ """Outpost API Views""" from dataclasses import asdict -from django.urls import reverse +from django.utils.translation import gettext_lazy as _ from drf_yasg.utils import swagger_auto_schema -from rest_framework import mixins +from kubernetes.client.configuration import Configuration +from kubernetes.config.config_exception import ConfigException +from kubernetes.config.kube_config import load_kube_config_from_dict +from rest_framework import mixins, serializers from rest_framework.decorators import action from rest_framework.fields import BooleanField, CharField, SerializerMethodField from rest_framework.request import Request @@ -77,8 +80,7 @@ class ServiceConnectionViewSet( { "name": verbose_name(subclass), "description": subclass.__doc__, - "link": reverse("authentik_admin:outpost-service-connection-create") - + f"?type={subclass.__name__}", + "component": subclass().component, } ) return Response(TypeCreateSerializer(data, many=True).data) @@ -115,6 +117,24 @@ class DockerServiceConnectionViewSet(ModelViewSet): class KubernetesServiceConnectionSerializer(ServiceConnectionSerializer): """KubernetesServiceConnection Serializer""" + def validate_kubeconfig(self, kubeconfig): + """Validate kubeconfig by attempting to load it""" + if kubeconfig == {}: + if not self.validated_data["local"]: + raise serializers.ValidationError( + _( + "You can only use an empty kubeconfig when connecting to a local cluster." + ) + ) + # Empty kubeconfig is valid + return kubeconfig + config = Configuration() + try: + load_kube_config_from_dict(kubeconfig, client_configuration=config) + except ConfigException: + raise serializers.ValidationError(_("Invalid kubeconfig")) + return kubeconfig + class Meta: model = KubernetesServiceConnection diff --git a/authentik/outposts/forms.py b/authentik/outposts/forms.py deleted file mode 100644 index f8805eb75..000000000 --- a/authentik/outposts/forms.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Outpost forms""" -from django import forms -from django.core.exceptions import ValidationError -from django.utils.translation import gettext_lazy as _ -from kubernetes.client.configuration import Configuration -from kubernetes.config.config_exception import ConfigException -from kubernetes.config.kube_config import load_kube_config_from_dict - -from authentik.admin.fields import CodeMirrorWidget, YAMLField -from authentik.crypto.models import CertificateKeyPair -from authentik.outposts.models import ( - DockerServiceConnection, - KubernetesServiceConnection, -) - - -class DockerServiceConnectionForm(forms.ModelForm): - """Docker service-connection form""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["tls_authentication"].queryset = CertificateKeyPair.objects.filter( - key_data__isnull=False - ) - - class Meta: - - model = DockerServiceConnection - fields = ["name", "local", "url", "tls_verification", "tls_authentication"] - widgets = { - "name": forms.TextInput, - "url": forms.TextInput, - } - labels = { - "url": _("URL"), - "tls_verification": _("TLS Verification Certificate"), - "tls_authentication": _("TLS Authentication Certificate"), - } - - -class KubernetesServiceConnectionForm(forms.ModelForm): - """Kubernetes service-connection form""" - - def clean_kubeconfig(self): - """Validate kubeconfig by attempting to load it""" - kubeconfig = self.cleaned_data["kubeconfig"] - if kubeconfig == {}: - if not self.cleaned_data["local"]: - raise ValidationError( - _("You can only use an empty kubeconfig when local is enabled.") - ) - # Empty kubeconfig is valid - return kubeconfig - config = Configuration() - try: - load_kube_config_from_dict(kubeconfig, client_configuration=config) - except ConfigException: - raise ValidationError(_("Invalid kubeconfig")) - return kubeconfig - - class Meta: - - model = KubernetesServiceConnection - fields = [ - "name", - "local", - "kubeconfig", - ] - widgets = { - "name": forms.TextInput, - "kubeconfig": CodeMirrorWidget, - } - field_classes = { - "kubeconfig": YAMLField, - } diff --git a/authentik/outposts/models.py b/authentik/outposts/models.py index 9b0680010..442f81371 100644 --- a/authentik/outposts/models.py +++ b/authentik/outposts/models.py @@ -1,14 +1,13 @@ """Outpost models""" from dataclasses import asdict, dataclass, field from datetime import datetime -from typing import Iterable, Optional, Type, Union +from typing import Iterable, Optional, Union from uuid import uuid4 from dacite import from_dict from django.core.cache import cache from django.db import models, transaction from django.db.models.base import Model -from django.forms.models import ModelForm from django.utils.translation import gettext_lazy as _ from docker.client import DockerClient from docker.errors import DockerException @@ -132,8 +131,8 @@ class OutpostServiceConnection(models.Model): raise NotImplementedError @property - def form(self) -> Type[ModelForm]: - """Return Form class used to edit this object""" + def component(self) -> str: + """Return component used to edit this object""" raise NotImplementedError class Meta: @@ -180,10 +179,8 @@ class DockerServiceConnection(OutpostServiceConnection): ) @property - def form(self) -> Type[ModelForm]: - from authentik.outposts.forms import DockerServiceConnectionForm - - return DockerServiceConnectionForm + def component(self) -> str: + return "ak-service-connection-docker-form" def __str__(self) -> str: return f"Docker Service-Connection {self.name}" @@ -237,10 +234,8 @@ class KubernetesServiceConnection(OutpostServiceConnection): ) @property - def form(self) -> Type[ModelForm]: - from authentik.outposts.forms import KubernetesServiceConnectionForm - - return KubernetesServiceConnectionForm + def component(self) -> str: + return "ak-service-connection-kubernetes-form" def __str__(self) -> str: return f"Kubernetes Service-Connection {self.name}" diff --git a/web/src/api/legacy.ts b/web/src/api/legacy.ts index bd6a9dc33..7a04128d8 100644 --- a/web/src/api/legacy.ts +++ b/web/src/api/legacy.ts @@ -12,10 +12,6 @@ export class AdminURLManager { return `/administration/property-mappings/${rest}`; } - static outpostServiceConnections(rest: string): string { - return `/administration/outpost_service_connections/${rest}`; - } - static stages(rest: string): string { return `/administration/stages/${rest}`; } diff --git a/web/src/pages/outposts/ServiceConnectionDockerForm.ts b/web/src/pages/outposts/ServiceConnectionDockerForm.ts new file mode 100644 index 000000000..3e9010f83 --- /dev/null +++ b/web/src/pages/outposts/ServiceConnectionDockerForm.ts @@ -0,0 +1,111 @@ +import { CryptoApi, DockerServiceConnection, OutpostsApi } 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-service-connection-docker-form") +export class ServiceConnectionDockerForm extends Form { + + set scUUID(value: string) { + new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsDockerRead({ + uuid: value, + }).then(sc => { + this.sc = sc; + }); + } + + @property({attribute: false}) + sc?: DockerServiceConnection; + + getSuccessMessage(): string { + if (this.sc) { + return gettext("Successfully updated service-connection."); + } else { + return gettext("Successfully created service-connection."); + } + } + + send = (data: DockerServiceConnection): Promise => { + if (this.sc) { + return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsDockerUpdate({ + uuid: this.sc.pk || "", + data: data + }); + } else { + return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsDockerCreate({ + data: data + }); + } + }; + + renderForm(): TemplateResult { + return html`
+ + + + +
+ + +
+

${gettext("If enabled, use the local connection. Required Docker socket/Kubernetes Integration.")}

+
+ + +

${gettext("Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system.")}

+
+ + +

${gettext("CA which the endpoint's Certificate is verified against. Can be left empty for no validation.")}

+
+ + +

${gettext("Certificate/Key used for authentication. Can be left empty for no authentication.")}

+
+
`; + } + +} diff --git a/web/src/pages/outposts/ServiceConnectionKubernetesForm.ts b/web/src/pages/outposts/ServiceConnectionKubernetesForm.ts new file mode 100644 index 000000000..5fc86dc91 --- /dev/null +++ b/web/src/pages/outposts/ServiceConnectionKubernetesForm.ts @@ -0,0 +1,73 @@ +import { KubernetesServiceConnection, OutpostsApi } 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 { ifDefined } from "lit-html/directives/if-defined"; +import "../../elements/forms/HorizontalFormElement"; +import "../../elements/CodeMirror"; +import YAML from "yaml"; + +@customElement("ak-service-connection-kubernetes-form") +export class ServiceConnectionKubernetesForm extends Form { + + set scUUID(value: string) { + new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsKubernetesRead({ + uuid: value, + }).then(sc => { + this.sc = sc; + }); + } + + @property({attribute: false}) + sc?: KubernetesServiceConnection; + + getSuccessMessage(): string { + if (this.sc) { + return gettext("Successfully updated service-connection."); + } else { + return gettext("Successfully created service-connection."); + } + } + + send = (data: KubernetesServiceConnection): Promise => { + if (this.sc) { + return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsKubernetesUpdate({ + uuid: this.sc.pk || "", + data: data + }); + } else { + return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsKubernetesCreate({ + data: data + }); + } + }; + + renderForm(): TemplateResult { + return html`
+ + + + +
+ + +
+

${gettext("If enabled, use the local connection. Required Docker socket/Kubernetes Integration.")}

+
+ + + + +
`; + } + +} diff --git a/web/src/pages/outposts/OutpostServiceConnectionListPage.ts b/web/src/pages/outposts/ServiceConnectionListPage.ts similarity index 70% rename from web/src/pages/outposts/OutpostServiceConnectionListPage.ts rename to web/src/pages/outposts/ServiceConnectionListPage.ts index bacf5cf5c..3e8460ca5 100644 --- a/web/src/pages/outposts/OutpostServiceConnectionListPage.ts +++ b/web/src/pages/outposts/ServiceConnectionListPage.ts @@ -10,11 +10,15 @@ import "../../elements/buttons/SpinnerButton"; import "../../elements/buttons/ModalButton"; import "../../elements/buttons/Dropdown"; import "../../elements/forms/DeleteForm"; +import "../../elements/forms/ModalForm"; +import "./ServiceConnectionKubernetesForm"; +import "./ServiceConnectionDockerForm"; import { until } from "lit-html/directives/until"; import { PAGE_SIZE } from "../../constants"; import { OutpostsApi, ServiceConnection } from "authentik-api"; import { DEFAULT_CONFIG } from "../../api/Config"; -import { AdminURLManager } from "../../api/legacy"; +import "../../elements/forms/ProxyForm"; +import { ifDefined } from "lit-html/directives/if-defined"; @customElement("ak-outpost-service-connection-list") export class OutpostServiceConnectionListPage extends TablePage { @@ -68,12 +72,28 @@ export class OutpostServiceConnectionListPage extends TablePage ${gettext("Unhealthy")}`; }), html``)}`, html` - - + + + ${gettext("Update")} + + + ${gettext(`Update ${item.verboseName}`)} + + + + + { return types.map((type) => { return html`
  • - - -
    -
    +
  • `; }); }), html``)} diff --git a/web/src/routes.ts b/web/src/routes.ts index 1a7057ffa..1b7934a98 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -14,7 +14,7 @@ import "./pages/flows/FlowViewPage"; import "./pages/groups/GroupListPage"; import "./pages/LibraryPage"; import "./pages/outposts/OutpostListPage"; -import "./pages/outposts/OutpostServiceConnectionListPage"; +import "./pages/outposts/ServiceConnectionListPage"; import "./pages/policies/PolicyListPage"; import "./pages/property-mappings/PropertyMappingListPage"; import "./pages/providers/ProviderListPage";