From a3d76c3e60237133e09446510d07da704360e367 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 19 Jul 2024 15:46:35 +0200 Subject: [PATCH] :sparkles: [#74] Update the admin interface Updated the admin pages for DigiD/EH configuration to now point to the place to add new/extra certificates (or manage existing ones). The validation before saving now requires that at least one certificate is defined beforehand, which matches the existing validation rules that the certificate had to be selected. --- digid_eherkenning/admin.py | 201 +++++++++++------------ digid_eherkenning/models/base.py | 26 +-- digid_eherkenning/models/certificates.py | 3 +- 3 files changed, 117 insertions(+), 113 deletions(-) diff --git a/digid_eherkenning/admin.py b/digid_eherkenning/admin.py index b3c5a99..aa7d3d4 100644 --- a/digid_eherkenning/admin.py +++ b/digid_eherkenning/admin.py @@ -1,6 +1,8 @@ from datetime import datetime from django.contrib import admin +from django.urls import reverse +from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from privates.admin import PrivateMediaMixin @@ -8,6 +10,7 @@ from solo.admin import SingletonModelAdmin from .models import ConfigCertificate, DigidConfiguration, EherkenningConfiguration +from .models.base import BaseConfiguration class CustomPrivateFileWidget(PrivateFileWidget): @@ -18,14 +21,39 @@ class CustomPrivateMediaMixin(PrivateMediaMixin): private_media_file_widget = CustomPrivateFileWidget -@admin.register(DigidConfiguration) -class DigidConfigurationAdmin(CustomPrivateMediaMixin, SingletonModelAdmin): - readonly_fields = ("idp_service_entity_id",) - fieldsets = ( +class BaseAdmin(CustomPrivateMediaMixin, SingletonModelAdmin): + readonly_fields = ( + "link_to_certificates", + "idp_service_entity_id", + ) + private_media_fields = ("idp_metadata_file",) + + @admin.display(description=_("certificates")) + def link_to_certificates(self, obj: BaseConfiguration) -> str: + path = reverse( + "admin:digid_eherkenning_configcertificate_changelist", + current_app=self.admin_site.name, + ) + config_type = obj._as_config_type() + qs = ConfigCertificate.objects.filter(config_type=config_type) + url = f"{path}?config_type__exact={config_type}" + return format_html( + '{label}', + url=url, + config_type=config_type.value, + label=_("Manage ({count})").format(count=qs.count()), + ) + + +def _fieldset_factory(middle): + """ + Output custom fieldsets (model-specific) between fixed shared field(set)s. + """ + head = [ ( _("X.509 Certificate"), { - "fields": ("certificate",), + "fields": ("link_to_certificates",), }, ), ( @@ -52,18 +80,8 @@ class DigidConfigurationAdmin(CustomPrivateMediaMixin, SingletonModelAdmin): ), }, ), - ( - _("Service details"), - { - "fields": ( - "service_name", - "service_description", - "requested_attributes", - "attribute_consuming_service_index", - "slo", - ), - }, - ), + ] + tail = [ ( _("Organization details"), { @@ -75,99 +93,80 @@ class DigidConfigurationAdmin(CustomPrivateMediaMixin, SingletonModelAdmin): ), }, ), + ] + + return tuple(head + list(middle) + tail) + + +@admin.register(DigidConfiguration) +class DigidConfigurationAdmin(BaseAdmin): + fieldsets = _fieldset_factory( + [ + ( + _("Service details"), + { + "fields": ( + "service_name", + "service_description", + "requested_attributes", + "attribute_consuming_service_index", + "slo", + ), + }, + ), + ] ) change_form_template = "admin/digid_eherkenning/digidconfiguration/change_form.html" - private_media_fields = ("idp_metadata_file",) @admin.register(EherkenningConfiguration) -class EherkenningConfigurationAdmin(CustomPrivateMediaMixin, SingletonModelAdmin): - readonly_fields = ("idp_service_entity_id",) - fieldsets = ( - ( - _("X.509 Certificate"), - { - "fields": ("certificate",), - }, - ), - ( - _("Identity provider"), - { - "fields": ( - "metadata_file_source", - "idp_service_entity_id", - "idp_metadata_file", - ), - }, - ), - ( - _("SAML configuration"), - { - "fields": ( - "entity_id", - "base_url", - "artifact_resolve_content_type", - "want_assertions_signed", - "want_assertions_encrypted", - "signature_algorithm", - "digest_algorithm", - ), - }, - ), - ( - _("Service details"), - { - "fields": ( - "service_name", - "service_description", - "oin", - "makelaar_id", - "privacy_policy", - "service_language", - ), - }, - ), - ( - _("eHerkenning"), - { - "fields": ( - "eh_requested_attributes", - "eh_attribute_consuming_service_index", - "eh_service_uuid", - "eh_service_instance_uuid", - "eh_loa", - ), - }, - ), - ( - _("eIDAS"), - { - "fields": ( - "no_eidas", - "eidas_requested_attributes", - "eidas_attribute_consuming_service_index", - "eidas_service_uuid", - "eidas_service_instance_uuid", - "eidas_loa", - ), - }, - ), - ( - _("Organization details"), - { - "fields": ( - "technical_contact_person_telephone", - "technical_contact_person_email", - "organization_url", - "organization_name", - ), - }, - ), +class EherkenningConfigurationAdmin(BaseAdmin): + fieldsets = _fieldset_factory( + [ + ( + _("Service details"), + { + "fields": ( + "service_name", + "service_description", + "oin", + "makelaar_id", + "privacy_policy", + "service_language", + ), + }, + ), + ( + _("eHerkenning"), + { + "fields": ( + "eh_requested_attributes", + "eh_attribute_consuming_service_index", + "eh_service_uuid", + "eh_service_instance_uuid", + "eh_loa", + ), + }, + ), + ( + _("eIDAS"), + { + "fields": ( + "no_eidas", + "eidas_requested_attributes", + "eidas_attribute_consuming_service_index", + "eidas_service_uuid", + "eidas_service_instance_uuid", + "eidas_loa", + ), + }, + ), + ] ) + change_form_template = ( "admin/digid_eherkenning/eherkenningconfiguration/change_form.html" ) - private_media_fields = ("idp_metadata_file",) @admin.register(ConfigCertificate) diff --git a/digid_eherkenning/models/base.py b/digid_eherkenning/models/base.py index 3c0c20b..9848561 100644 --- a/digid_eherkenning/models/base.py +++ b/digid_eherkenning/models/base.py @@ -9,16 +9,15 @@ from privates.fields import PrivateMediaFileField from solo.models import SingletonModel -from ..choices import DigestAlgorithms, SignatureAlgorithms, XMLContentTypes +from ..choices import ( + ConfigTypes, + DigestAlgorithms, + SignatureAlgorithms, + XMLContentTypes, +) from .certificates import ConfigCertificate -class ConfigurationManager(models.Manager): - def get_queryset(self): - qs = super().get_queryset() - return qs.select_related("certificate") - - class BaseConfiguration(SingletonModel): idp_metadata_file = PrivateMediaFileField( _("identity provider metadata"), @@ -157,8 +156,6 @@ class BaseConfiguration(SingletonModel): max_length=100, ) - objects = ConfigurationManager() - class Meta: abstract = True @@ -234,4 +231,13 @@ def clean(self): # require that a certificate is configured if not ConfigCertificate.objects.for_config(self).exists(): - raise ValidationError(_("You must select a certificate")) + raise ValidationError( + _( + "You must prepare at least one certificate for the {verbose_name}." + ).format(verbose_name=self._meta.verbose_name) + ) + + @classmethod + def _as_config_type(cls) -> ConfigTypes: + opts = cls._meta + return ConfigTypes(f"{opts.app_label}.{opts.object_name}") diff --git a/digid_eherkenning/models/certificates.py b/digid_eherkenning/models/certificates.py index a881892..ab4d5c0 100644 --- a/digid_eherkenning/models/certificates.py +++ b/digid_eherkenning/models/certificates.py @@ -28,8 +28,7 @@ class ConfigCertificateQuerySet(models.QuerySet): def for_config(self, config: _AnyDigiD | _AnyEH): - opts = config._meta - config_type = ConfigTypes(f"{opts.app_label}.{opts.object_name}") + config_type = config._as_config_type() return self.filter(config_type=config_type)