diff --git a/authentik/admin/templates/administration/certificatekeypair/generate.html b/authentik/admin/templates/administration/certificatekeypair/generate.html deleted file mode 100644 index 6af5c916c..000000000 --- a/authentik/admin/templates/administration/certificatekeypair/generate.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends base_template|default:"generic/form.html" %} - -{% load authentik_utils %} -{% load i18n %} - -{% block above_form %} -

- {% trans 'Generate Certificate-Key Pair' %} -

-{% endblock %} - -{% block action %} -{% trans 'Generate Certificate-Key Pair' %} -{% endblock %} diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index f581a0a89..59c29d726 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -3,7 +3,6 @@ from django.urls import path from authentik.admin.views import ( applications, - certificate_key_pair, events_notifications_rules, events_notifications_transports, flows, @@ -151,22 +150,6 @@ urlpatterns = [ property_mappings.PropertyMappingTestView.as_view(), name="property-mapping-test", ), - # Certificate-Key Pairs - path( - "crypto/certificates/create/", - certificate_key_pair.CertificateKeyPairCreateView.as_view(), - name="certificatekeypair-create", - ), - path( - "crypto/certificates/generate/", - certificate_key_pair.CertificateKeyPairGenerateView.as_view(), - name="certificatekeypair-generate", - ), - path( - "crypto/certificates//update/", - certificate_key_pair.CertificateKeyPairUpdateView.as_view(), - name="certificatekeypair-update", - ), # Outposts path( "outposts/create/", diff --git a/authentik/admin/views/certificate_key_pair.py b/authentik/admin/views/certificate_key_pair.py deleted file mode 100644 index a08d334b2..000000000 --- a/authentik/admin/views/certificate_key_pair.py +++ /dev/null @@ -1,81 +0,0 @@ -"""authentik CertificateKeyPair 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.http.response import HttpResponse -from django.urls import reverse_lazy -from django.utils.translation import gettext as _ -from django.views.generic import UpdateView -from django.views.generic.edit import FormView -from guardian.mixins import PermissionRequiredMixin - -from authentik.crypto.builder import CertificateBuilder -from authentik.crypto.forms import ( - CertificateKeyPairForm, - CertificateKeyPairGenerateForm, -) -from authentik.crypto.models import CertificateKeyPair -from authentik.lib.views import CreateAssignPermView - - -class CertificateKeyPairCreateView( - SuccessMessageMixin, - LoginRequiredMixin, - DjangoPermissionRequiredMixin, - CreateAssignPermView, -): - """Create new CertificateKeyPair""" - - model = CertificateKeyPair - form_class = CertificateKeyPairForm - permission_required = "authentik_crypto.add_certificatekeypair" - - template_name = "generic/create.html" - success_url = reverse_lazy("authentik_core:if-admin") - success_message = _("Successfully created Certificate-Key Pair") - - -class CertificateKeyPairGenerateView( - SuccessMessageMixin, - LoginRequiredMixin, - DjangoPermissionRequiredMixin, - FormView, -): - """Generate new CertificateKeyPair""" - - model = CertificateKeyPair - form_class = CertificateKeyPairGenerateForm - permission_required = "authentik_crypto.add_certificatekeypair" - - template_name = "administration/certificatekeypair/generate.html" - success_url = reverse_lazy("authentik_core:if-admin") - success_message = _("Successfully generated Certificate-Key Pair") - - def form_valid(self, form: CertificateKeyPairGenerateForm) -> HttpResponse: - builder = CertificateBuilder() - builder.common_name = form.data["common_name"] - builder.build( - subject_alt_names=form.data.get("subject_alt_name", "").split(","), - validity_days=int(form.data["validity_days"]), - ) - builder.save() - return super().form_valid(form) - - -class CertificateKeyPairUpdateView( - SuccessMessageMixin, - LoginRequiredMixin, - PermissionRequiredMixin, - UpdateView, -): - """Update certificatekeypair""" - - model = CertificateKeyPair - form_class = CertificateKeyPairForm - permission_required = "authentik_crypto.change_certificatekeypair" - - template_name = "generic/update.html" - success_url = reverse_lazy("authentik_core:if-admin") - success_message = _("Successfully updated Certificate-Key Pair") diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py index 66076fe6c..13ce217c0 100644 --- a/authentik/core/api/applications.py +++ b/authentik/core/api/applications.py @@ -109,7 +109,7 @@ class ApplicationViewSet(ModelViewSet): return self.get_paginated_response(serializer.data) @permission_required( - "authentik_core.view_application", "authentik_events.view_event" + "authentik_core.view_application", ["authentik_events.view_event"] ) @swagger_auto_schema(responses={200: CoordinateSerializer(many=True)}) @action(detail=True) diff --git a/authentik/crypto/forms.py b/authentik/crypto/forms.py deleted file mode 100644 index f289cc207..000000000 --- a/authentik/crypto/forms.py +++ /dev/null @@ -1,64 +0,0 @@ -"""authentik Crypto forms""" -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.serialization import load_pem_private_key -from cryptography.x509 import load_pem_x509_certificate -from django import forms -from django.utils.translation import gettext_lazy as _ - -from authentik.crypto.models import CertificateKeyPair - - -class CertificateKeyPairGenerateForm(forms.Form): - """CertificateKeyPair generation form""" - - common_name = forms.CharField() - subject_alt_name = forms.CharField(required=False, label=_("Subject-alt name")) - validity_days = forms.IntegerField(initial=365) - - -class CertificateKeyPairForm(forms.ModelForm): - """CertificateKeyPair Form""" - - def clean_certificate_data(self): - """Verify that input is a valid PEM x509 Certificate""" - certificate_data = self.cleaned_data["certificate_data"] - try: - load_pem_x509_certificate( - certificate_data.encode("utf-8"), default_backend() - ) - except ValueError: - raise forms.ValidationError("Unable to load certificate.") - return certificate_data - - def clean_key_data(self): - """Verify that input is a valid PEM RSA Key""" - key_data = self.cleaned_data["key_data"] - # Since this field is optional, data can be empty. - if key_data != "": - try: - load_pem_private_key( - str.encode("\n".join([x.strip() for x in key_data.split("\n")])), - password=None, - backend=default_backend(), - ) - except ValueError: - raise forms.ValidationError("Unable to load private key.") - return key_data - - class Meta: - - model = CertificateKeyPair - fields = [ - "name", - "certificate_data", - "key_data", - ] - widgets = { - "name": forms.TextInput(), - "certificate_data": forms.Textarea(attrs={"class": "monospaced"}), - "key_data": forms.Textarea(attrs={"class": "monospaced"}), - } - labels = { - "certificate_data": _("Certificate"), - "key_data": _("Private Key"), - } diff --git a/authentik/crypto/tests.py b/authentik/crypto/tests.py index 1b43806d2..f42f32172 100644 --- a/authentik/crypto/tests.py +++ b/authentik/crypto/tests.py @@ -2,31 +2,12 @@ from django.test import TestCase from authentik.crypto.api import CertificateKeyPairSerializer -from authentik.crypto.forms import CertificateKeyPairForm from authentik.crypto.models import CertificateKeyPair class TestCrypto(TestCase): """Test Crypto validation""" - def test_form(self): - """Test form validation""" - keypair = CertificateKeyPair.objects.first() - self.assertTrue( - CertificateKeyPairForm( - { - "name": keypair.name, - "certificate_data": keypair.certificate_data, - "key_data": keypair.key_data, - } - ).is_valid() - ) - self.assertFalse( - CertificateKeyPairForm( - {"name": keypair.name, "certificate_data": "test", "key_data": "test"} - ).is_valid() - ) - def test_serializer(self): """Test API Validation""" keypair = CertificateKeyPair.objects.first() diff --git a/web/src/api/legacy.ts b/web/src/api/legacy.ts index e921b689e..76dbcd62b 100644 --- a/web/src/api/legacy.ts +++ b/web/src/api/legacy.ts @@ -4,10 +4,6 @@ export class AdminURLManager { return `/administration/applications/${rest}`; } - static cryptoCertificates(rest: string): string { - return `/administration/crypto/certificates/${rest}`; - } - static policies(rest: string): string { return `/administration/policies/${rest}`; } diff --git a/web/src/elements/Divider.ts b/web/src/elements/Divider.ts new file mode 100644 index 000000000..d354139f2 --- /dev/null +++ b/web/src/elements/Divider.ts @@ -0,0 +1,37 @@ +import { css, CSSResult, customElement, html, LitElement, TemplateResult } from "lit-element"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; +import AKGlobal from "../authentik.css"; + +@customElement("ak-divider") +export class Divider extends LitElement { + + static get styles(): CSSResult[] { + return [PFBase, AKGlobal, css` + .separator { + display: flex; + align-items: center; + text-align: center; + } + + .separator::before, + .separator::after { + content: ''; + flex: 1; + border-bottom: 1px solid var(--pf-global--Color--100); + } + + .separator:not(:empty)::before { + margin-right: .25em; + } + + .separator:not(:empty)::after { + margin-left: .25em; + } + `]; + } + + render(): TemplateResult { + return html`
`; + } + +} diff --git a/web/src/pages/crypto/CertificateGenerateForm.ts b/web/src/pages/crypto/CertificateGenerateForm.ts new file mode 100644 index 000000000..f435b434a --- /dev/null +++ b/web/src/pages/crypto/CertificateGenerateForm.ts @@ -0,0 +1,37 @@ +import { CertificateGeneration, CryptoApi } 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 "../../elements/forms/HorizontalFormElement"; + +@customElement("ak-crypto-certificate-generate-form") +export class CertificateKeyPairForm extends Form { + + getSuccessMessage(): string { + return gettext("Successfully generated certificate-key pair."); + } + + send = (data: CertificateGeneration): Promise => { + return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsGenerate({ + data: data + }); + }; + + renderForm(): TemplateResult { + return html`
+ + + + + +

${gettext("Optional, comma-separated SubjectAlt Names.")}

+
+ + + +
`; + } + +} diff --git a/web/src/pages/crypto/CertificateKeyPairForm.ts b/web/src/pages/crypto/CertificateKeyPairForm.ts new file mode 100644 index 000000000..9ccb58e39 --- /dev/null +++ b/web/src/pages/crypto/CertificateKeyPairForm.ts @@ -0,0 +1,56 @@ +import { CertificateKeyPair, CoreApi, CryptoApi, Group } 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 "../../elements/Divider"; + +@customElement("ak-crypto-certificate-form") +export class CertificateKeyPairForm extends Form { + + @property({attribute: false}) + keyPair?: CertificateKeyPair; + + getSuccessMessage(): string { + if (this.keyPair) { + return gettext("Successfully updated certificate-key pair."); + } else { + return gettext("Successfully created certificate-key pair."); + } + } + + send = (data: CertificateKeyPair): Promise => { + if (this.keyPair) { + return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsPartialUpdate({ + kpUuid: this.keyPair.pk || "", + data: data + }); + } else { + return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsCreate({ + data: data + }); + } + }; + + renderForm(): TemplateResult { + return html`
+ + + + ${this.keyPair ? html`${gettext("Only change the fields below if you want to overwrite their values.")}` : html``} + + +

${gettext("PEM-encoded Certificate data.")}

+
+ + +

${gettext("Optional Private Key. If this is set, you can use this keypair for encryption.")}

+
+
`; + } + +} diff --git a/web/src/pages/crypto/CertificateKeyPairListPage.ts b/web/src/pages/crypto/CertificateKeyPairListPage.ts index 8b2ab5835..d08ddba34 100644 --- a/web/src/pages/crypto/CertificateKeyPairListPage.ts +++ b/web/src/pages/crypto/CertificateKeyPairListPage.ts @@ -6,12 +6,13 @@ import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList import { CryptoApi, CertificateKeyPair } from "authentik-api"; -import "../../elements/buttons/ModalButton"; +import "../../elements/forms/ModalForm"; import "../../elements/buttons/SpinnerButton"; import "../../elements/forms/DeleteForm"; +import "./CertificateKeyPairForm"; +import "./CertificateGenerateForm"; import { TableColumn } from "../../elements/table/Table"; import { PAGE_SIZE } from "../../constants"; -import { AdminURLManager } from "../../api/legacy"; import { DEFAULT_CONFIG } from "../../api/Config"; @customElement("ak-crypto-certificate-list") @@ -62,12 +63,19 @@ export class CertificateKeyPairListPage extends TablePage { html`${gettext(item.privateKeyAvailable ? "Yes" : "No")}`, html`${item.certExpiry?.toLocaleString()}`, html` - - + + + ${gettext("Update")} + + + ${gettext("Update Certificate-Key Pair")} + + + + + { renderToolbar(): TemplateResult { return html` - - + + ${gettext("Create")} - -
-
- - + + + ${gettext("Create Certificate-Key Pair")} + + + + + + + ${gettext("Generate")} - -
-
+ + + ${gettext("Generate Certificate-Key Pair")} + + + + + ${super.renderToolbar()} `; }