web/admin: migrate crypto/certificatekeypair to web
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -1,14 +0,0 @@ | ||||
| {% extends base_template|default:"generic/form.html" %} | ||||
|  | ||||
| {% load authentik_utils %} | ||||
| {% load i18n %} | ||||
|  | ||||
| {% block above_form %} | ||||
| <h1> | ||||
|     {% trans 'Generate Certificate-Key Pair' %} | ||||
| </h1> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block action %} | ||||
| {% trans 'Generate Certificate-Key Pair' %} | ||||
| {% endblock %} | ||||
| @ -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/<uuid:pk>/update/", | ||||
|         certificate_key_pair.CertificateKeyPairUpdateView.as_view(), | ||||
|         name="certificatekeypair-update", | ||||
|     ), | ||||
|     # Outposts | ||||
|     path( | ||||
|         "outposts/create/", | ||||
|  | ||||
| @ -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") | ||||
| @ -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) | ||||
|  | ||||
| @ -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"), | ||||
|         } | ||||
| @ -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() | ||||
|  | ||||
| @ -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}`; | ||||
|     } | ||||
|  | ||||
							
								
								
									
										37
									
								
								web/src/elements/Divider.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								web/src/elements/Divider.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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`<div class="separator"><slot></slot></div>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										37
									
								
								web/src/pages/crypto/CertificateGenerateForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								web/src/pages/crypto/CertificateGenerateForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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<CertificateGeneration> { | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         return gettext("Successfully generated certificate-key pair."); | ||||
|     } | ||||
|  | ||||
|     send = (data: CertificateGeneration): Promise<CertificateGeneration> => { | ||||
|         return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsGenerate({ | ||||
|             data: data | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${gettext("Common Name")} ?required=${true}> | ||||
|                 <input type="text" name="commonName" class="pf-c-form-control" required=""> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${gettext("Subject-alt name")}> | ||||
|                 <input class="pf-c-form-control" type="text" name="subjectAltName"> | ||||
|                 <p class="pf-c-form__helper-text">${gettext("Optional, comma-separated SubjectAlt Names.")}</p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${gettext("Validity days")} ?required=${true}> | ||||
|                 <input class="pf-c-form-control" type="number" name="validityDays" value="365"> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										56
									
								
								web/src/pages/crypto/CertificateKeyPairForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								web/src/pages/crypto/CertificateKeyPairForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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<CertificateKeyPair> { | ||||
|  | ||||
|     @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<CertificateKeyPair> => { | ||||
|         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`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${gettext("Name")} ?required=${true}> | ||||
|                 <input type="text" name="name" value="${ifDefined(this.keyPair?.name)}" class="pf-c-form-control" required=""> | ||||
|             </ak-form-element-horizontal> | ||||
|             ${this.keyPair ? html`<ak-divider>${gettext("Only change the fields below if you want to overwrite their values.")}</ak-divider>` : html``} | ||||
|             <ak-form-element-horizontal label=${gettext("Certificate")} ?required=${true}> | ||||
|                 <textarea class="pf-c-form-control" type="text" name="certificateData">${ifDefined(this.keyPair?.certificateData)}</textarea> | ||||
|                 <p class="pf-c-form__helper-text">${gettext("PEM-encoded Certificate data.")}</p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${gettext("Private Key")}> | ||||
|                 <textarea class="pf-c-form-control" type="text" name="keyData">${ifDefined(this.keyPair?.keyData)}</textarea> | ||||
|                 <p class="pf-c-form__helper-text">${gettext("Optional Private Key. If this is set, you can use this keypair for encryption.")}</p> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -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<CertificateKeyPair> { | ||||
|             html`${gettext(item.privateKeyAvailable ? "Yes" : "No")}`, | ||||
|             html`${item.certExpiry?.toLocaleString()}`, | ||||
|             html` | ||||
|             <ak-modal-button href="${AdminURLManager.cryptoCertificates(`${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 Certificate-Key Pair")} | ||||
|                 </span> | ||||
|                 <ak-crypto-certificate-form slot="form" .keyPair=${item}> | ||||
|                 </ak-crypto-certificate-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-secondary"> | ||||
|                     ${gettext("Edit")} | ||||
|                 </ak-spinner-button> | ||||
|                 <div slot="modal"></div> | ||||
|             </ak-modal-button> | ||||
|                 </button> | ||||
|             </ak-forms-modal> | ||||
|             <ak-forms-delete | ||||
|                 .obj=${item} | ||||
|                 objectLabel=${gettext("Certificate-Key Pair")} | ||||
| @ -113,18 +121,32 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> { | ||||
|  | ||||
|     renderToolbar(): TemplateResult { | ||||
|         return html` | ||||
|         <ak-modal-button href=${AdminURLManager.cryptoCertificates("create/")}> | ||||
|             <ak-spinner-button slot="trigger" class="pf-m-primary"> | ||||
|         <ak-forms-modal> | ||||
|             <span slot="submit"> | ||||
|                 ${gettext("Create")} | ||||
|             </ak-spinner-button> | ||||
|             <div slot="modal"></div> | ||||
|         </ak-modal-button> | ||||
|         <ak-modal-button href=${AdminURLManager.cryptoCertificates("generate/")}> | ||||
|             <ak-spinner-button slot="trigger" class="pf-m-secondary"> | ||||
|             </span> | ||||
|             <span slot="header"> | ||||
|                 ${gettext("Create Certificate-Key Pair")} | ||||
|             </span> | ||||
|             <ak-crypto-certificate-form slot="form"> | ||||
|             </ak-crypto-certificate-form> | ||||
|             <button slot="trigger" class="pf-c-button pf-m-primary"> | ||||
|                 ${gettext("Create")} | ||||
|             </button> | ||||
|         </ak-forms-modal> | ||||
|         <ak-forms-modal> | ||||
|             <span slot="submit"> | ||||
|                 ${gettext("Generate")} | ||||
|             </ak-spinner-button> | ||||
|             <div slot="modal"></div> | ||||
|         </ak-modal-button> | ||||
|             </span> | ||||
|             <span slot="header"> | ||||
|                 ${gettext("Generate Certificate-Key Pair")} | ||||
|             </span> | ||||
|             <ak-crypto-certificate-generate-form slot="form"> | ||||
|             </ak-crypto-certificate-generate-form> | ||||
|             <button slot="trigger" class="pf-c-button pf-m-secondary"> | ||||
|                 ${gettext("Generate")} | ||||
|             </button> | ||||
|         </ak-forms-modal> | ||||
|         ${super.renderToolbar()} | ||||
|         `; | ||||
|     } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer