diff --git a/digid_eherkenning/admin.py b/digid_eherkenning/admin.py
index f8a949c..0270cf5 100644
--- a/digid_eherkenning/admin.py
+++ b/digid_eherkenning/admin.py
@@ -2,13 +2,23 @@
from django.utils.translation import gettext_lazy as _
from privates.admin import PrivateMediaMixin
+from privates.widgets import PrivateFileWidget
from solo.admin import SingletonModelAdmin
from .models import DigidConfiguration, EherkenningConfiguration
+class CustomPrivateFileWidget(PrivateFileWidget):
+ template_name = "admin/digid_eherkenning/widgets/custom_file_input.html"
+
+
+class CustomPrivateMediaMixin(PrivateMediaMixin):
+ private_media_file_widget = CustomPrivateFileWidget
+
+
@admin.register(DigidConfiguration)
-class DigidConfigurationAdmin(PrivateMediaMixin, SingletonModelAdmin):
+class DigidConfigurationAdmin(CustomPrivateMediaMixin, SingletonModelAdmin):
+ readonly_fields = ("idp_service_entity_id",)
fieldsets = (
(
_("X.509 Certificate"),
@@ -23,8 +33,9 @@ class DigidConfigurationAdmin(PrivateMediaMixin, SingletonModelAdmin):
_("Identity provider"),
{
"fields": (
- "idp_metadata_file",
+ "metadata_file_source",
"idp_service_entity_id",
+ "idp_metadata_file",
),
},
),
@@ -71,7 +82,8 @@ class DigidConfigurationAdmin(PrivateMediaMixin, SingletonModelAdmin):
@admin.register(EherkenningConfiguration)
-class EherkenningConfigurationAdmin(PrivateMediaMixin, SingletonModelAdmin):
+class EherkenningConfigurationAdmin(CustomPrivateMediaMixin, SingletonModelAdmin):
+ readonly_fields = ("idp_service_entity_id",)
fieldsets = (
(
_("X.509 Certificate"),
@@ -86,8 +98,9 @@ class EherkenningConfigurationAdmin(PrivateMediaMixin, SingletonModelAdmin):
_("Identity provider"),
{
"fields": (
- "idp_metadata_file",
+ "metadata_file_source",
"idp_service_entity_id",
+ "idp_metadata_file",
),
},
),
diff --git a/digid_eherkenning/management/commands/update_stored_metadata.py b/digid_eherkenning/management/commands/update_stored_metadata.py
new file mode 100644
index 0000000..5df4643
--- /dev/null
+++ b/digid_eherkenning/management/commands/update_stored_metadata.py
@@ -0,0 +1,30 @@
+from django.core.management import BaseCommand
+
+from digid_eherkenning.models.digid import DigidConfiguration
+from digid_eherkenning.models.eherkenning import EherkenningConfiguration
+
+
+class Command(BaseCommand):
+ help = "Updates the stored metadata file and prepopulates the db fields."
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ "config_model",
+ type=str,
+ choices=["digid", "eherkenning"],
+ help="Update the DigiD or Eherkenning configuration metadata.",
+ )
+
+ def handle(self, **options):
+ if options["config_model"] == "digid":
+ config = DigidConfiguration.get_solo()
+ elif options["config_model"] == "eherkenning":
+ config = EherkenningConfiguration.get_solo()
+
+ if config.metadata_file_source:
+ config.save()
+ self.stdout.write(self.style.SUCCESS("Update was successful"))
+ else:
+ self.stdout.write(
+ self.style.WARNING("Update failed, no metadata file source found")
+ )
diff --git a/digid_eherkenning/migrations/0006_digidconfiguration_metadata_file_source_and_more.py b/digid_eherkenning/migrations/0006_digidconfiguration_metadata_file_source_and_more.py
new file mode 100644
index 0000000..411f548
--- /dev/null
+++ b/digid_eherkenning/migrations/0006_digidconfiguration_metadata_file_source_and_more.py
@@ -0,0 +1,79 @@
+# Generated by Django 4.2.6 on 2023-10-20 08:40
+
+from django.db import migrations, models
+import privates.fields
+import privates.storages
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ (
+ "digid_eherkenning",
+ "0005_alter_eherkenningconfiguration_eh_service_instance_uuid_and_more",
+ ),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="digidconfiguration",
+ name="metadata_file_source",
+ field=models.URLField(
+ default="",
+ help_text="The URL-source where the XML metadata file can be retrieved from.",
+ max_length=255,
+ verbose_name="metadata file(XML) URL",
+ ),
+ ),
+ migrations.AddField(
+ model_name="eherkenningconfiguration",
+ name="metadata_file_source",
+ field=models.URLField(
+ default="",
+ help_text="The URL-source where the XML metadata file can be retrieved from.",
+ max_length=255,
+ verbose_name="metadata file(XML) URL",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="digidconfiguration",
+ name="idp_metadata_file",
+ field=privates.fields.PrivateMediaFileField(
+ blank=True,
+ help_text="The metadata file of the identity provider. This is auto populated from the configured source URL.",
+ storage=privates.storages.PrivateMediaFileSystemStorage(),
+ upload_to="",
+ verbose_name="identity provider metadata",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="digidconfiguration",
+ name="idp_service_entity_id",
+ field=models.CharField(
+ blank=True,
+ help_text="Example value: 'https://was-preprod1.digid.nl/saml/idp/metadata'. Note that this must match the 'entityID' attribute on the 'md:EntityDescriptor' node found in the Identity Provider's metadata. This is auto populated from the configured source URL.",
+ max_length=255,
+ verbose_name="identity provider service entity ID",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="eherkenningconfiguration",
+ name="idp_metadata_file",
+ field=privates.fields.PrivateMediaFileField(
+ blank=True,
+ help_text="The metadata file of the identity provider. This is auto populated from the configured source URL.",
+ storage=privates.storages.PrivateMediaFileSystemStorage(),
+ upload_to="",
+ verbose_name="identity provider metadata",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="eherkenningconfiguration",
+ name="idp_service_entity_id",
+ field=models.CharField(
+ blank=True,
+ help_text="Example value: 'https://was-preprod1.digid.nl/saml/idp/metadata'. Note that this must match the 'entityID' attribute on the 'md:EntityDescriptor' node found in the Identity Provider's metadata. This is auto populated from the configured source URL.",
+ max_length=255,
+ verbose_name="identity provider service entity ID",
+ ),
+ ),
+ ]
diff --git a/digid_eherkenning/models/base.py b/digid_eherkenning/models/base.py
index b24cdaa..2a31ec1 100644
--- a/digid_eherkenning/models/base.py
+++ b/digid_eherkenning/models/base.py
@@ -1,8 +1,11 @@
from django.core.exceptions import ValidationError
+from django.core.files.base import ContentFile
from django.db import models
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
+from onelogin.saml2.constants import OneLogin_Saml2_Constants
+from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
from privates.fields import PrivateMediaFileField
from simple_certmanager.models import Certificate
from solo.models import SingletonModel
@@ -29,17 +32,29 @@ class BaseConfiguration(SingletonModel):
)
idp_metadata_file = PrivateMediaFileField(
_("identity provider metadata"),
- blank=False,
- help_text=_("The metadata file of the identity provider."),
+ blank=True,
+ help_text=_(
+ "The metadata file of the identity provider. This is auto populated "
+ "from the configured source URL."
+ ),
)
idp_service_entity_id = models.CharField(
_("identity provider service entity ID"),
max_length=255,
- blank=False,
+ blank=True,
help_text=_(
"Example value: 'https://was-preprod1.digid.nl/saml/idp/metadata'. Note "
"that this must match the 'entityID' attribute on the "
- "'md:EntityDescriptor' node found in the Identity Provider's metadata."
+ "'md:EntityDescriptor' node found in the Identity Provider's metadata. "
+ "This is auto populated from the configured source URL."
+ ),
+ )
+ metadata_file_source = models.URLField(
+ _("metadata file(XML) URL"),
+ max_length=255,
+ default="",
+ help_text=_(
+ "The URL-source where the XML metadata file can be retrieved from."
),
)
want_assertions_signed = models.BooleanField(
@@ -166,7 +181,62 @@ class Meta:
def __str__(self):
return force_str(self._meta.verbose_name)
+ def populate_xml_fields(self, urls: dict[str, str], xml: str) -> None:
+ """
+ Populates the idp_metadata_file and idp_service_entity_id fields based on the
+ fetched xml metadata
+ """
+ self.idp_service_entity_id = urls["entityId"]
+ content = ContentFile(xml.encode("utf-8"))
+ self.idp_metadata_file.save("metadata.xml", content, save=False)
+
+ def process_metadata_from_xml_source(self) -> tuple[dict[str, str], str]:
+ """
+ Parses the xml metadata
+
+ :return a tuple of a dictionary with the useful urls and the xml string itself.
+ """
+ try:
+ xml = OneLogin_Saml2_IdPMetadataParser.get_metadata(
+ self.metadata_file_source
+ )
+ parsed_idp_metadata = OneLogin_Saml2_IdPMetadataParser.parse(
+ xml,
+ required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST,
+ required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST,
+ )
+ # python3-saml library does not use proper-namespaced exceptions
+ except Exception as exc:
+ raise ValidationError(
+ _("Failed to parse the metadata, got error: {err}").format(err=str(exc))
+ ) from exc
+
+ if not (idp := parsed_idp_metadata.get("idp")):
+ raise ValidationError(
+ _(
+ "Could not find any identity provider information in the metadata at the provided URL."
+ )
+ )
+
+ # sometimes the xml file contains urn instead of a url as an entity ID
+ # use the provided url instead
+ urls = {
+ "entityId": (
+ entity_id
+ if not (entity_id := idp.get("entityId")).startswith("urn:")
+ else self.metadata_file_source
+ ),
+ "sso_url": idp.get("singleSignOnService", {}).get("url"),
+ "slo_url": idp.get("singleLogoutService", {}).get("url"),
+ }
+
+ return (urls, xml)
+
def save(self, *args, **kwargs):
+ if self.metadata_file_source:
+ urls, xml = self.process_metadata_from_xml_source()
+ self.populate_xml_fields(urls, xml)
+
if self.base_url.endswith("/"):
self.base_url = self.base_url[:-1]
super().save(*args, **kwargs)
diff --git a/digid_eherkenning/templates/admin/digid_eherkenning/widgets/custom_file_input.html b/digid_eherkenning/templates/admin/digid_eherkenning/widgets/custom_file_input.html
new file mode 100644
index 0000000..3559aca
--- /dev/null
+++ b/digid_eherkenning/templates/admin/digid_eherkenning/widgets/custom_file_input.html
@@ -0,0 +1,14 @@
+
+
+{% if widget.is_initial %}
+ {% if download_allowed %}
+
{{ widget.initial_text }}: {{ display_value }}
+ {% else %}
+
{{ widget.initial_text }}: {{ display_value }}
+ {% endif %}
+ {% if not widget.required %}
+
+ {% endif %}
+
+{% endif %}
diff --git a/docs/cli.rst b/docs/cli.rst
index 56f2511..6f77f1e 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -9,6 +9,7 @@ django-digid-eherkenning ships with a couple of Django management commands:
* ``generate_digid_metadata``
* ``generate_eherkenning_metadata``
* ``generate_eherkenning_dienstcatalogus``
+* ``update_stored_metadata``
For details, call:
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 4a17790..095d73f 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -24,7 +24,7 @@ If this header is not set or empty, we instead get the value from ``REMOTE_ADDR`
**Protecting metadata endpoints**
-The metdata URLs are open by design to facilitate sharing these URLs with identity
+The metadata URLs are open by design to facilitate sharing these URLs with identity
providers or other interested parties. Because the metadata is generated on the fly,
there is a Denial-of-Service risk. We recommend to protect these URLs at the web-server
level by:
diff --git a/tests/conftest.py b/tests/conftest.py
index b348246..bce0893 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -12,6 +12,13 @@
DIGID_TEST_METADATA_FILE = BASE_DIR / "files" / "digid" / "metadata"
+DIGID_TEST_METADATA_FILE_SLO_POST = (
+ BASE_DIR / "files" / "digid" / "metadata_with_slo_POST"
+)
+DIGID_TEST_METADATA_FILE_SLO_POST_2 = (
+ BASE_DIR / "files" / "digid" / "metadata_with_slo_POST_2"
+)
+
DIGID_TEST_KEY_FILE = BASE_DIR / "files" / "snakeoil-cert" / "ssl-cert-snakeoil.key"
DIGID_TEST_CERTIFICATE_FILE = (
BASE_DIR / "files" / "snakeoil-cert" / "ssl-cert-snakeoil.pem"
diff --git a/tests/files/digid/metadata b/tests/files/digid/metadata
index fb153bc..3b56628 100644
--- a/tests/files/digid/metadata
+++ b/tests/files/digid/metadata
@@ -1,32 +1,83 @@
-DNT82s99BhXBIvewrpNSnBnuACmZFAKg7Ze+rZmflcQ=gDvJjFd221rI7Y6JT6IFNRr9L1JVQgXiOSx62zfy0Qx7wZTjx1ngs+eOcRloHEdIKBd/BCDQGl10VakEmmzB1OYcuR2V5mq7IdR+lIJb+eoKO1dhc6IK+F2vfWCUxphYUDbfWBE0U06YvSI4di2j0CISMXUbbNiO8DeFWI6/NYuiRimONpRomjwz1X+nR1Aaw58A8hrqiYKMZzMDHL2wJ7haK5ZKv3lWtACpSMYcdNXAzo2le9T0IyZjNUlkGtgHH2UyjDUL1OcZnvMSpd3lHFc6HkUfkbrGmJecNJWZyXT+7BH2HxaYFW0jQEYaTw7vyYatYf0HTNqfcN4VePU7Ww==2e9046aba2e95ed07efb655f6f50880ef686e5312e9046aba2e95ed07efb655f6f50880ef686e531MIICsjCCAZqgAwIBAgIJAJhvbn1Aal/aMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV
-BAMMBnVidW50dTAeFw0xNzEyMjkxNjI3NDhaFw0yNzEyMjcxNjI3NDhaMBExDzAN
-BgNVBAMMBnVidW50dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAObY
-RaZaguRBRowjJraDM8A7VpcWi+aIzYc0003gmz1j+/31pQ7QX12cCFAnyo+6bFlQ
-Or8/Qda24yIAsaaj96Z3iA4bWDUARLUD8N6gftI8sZwf1JRW6kgwYn3DiLdhDeKd
-8LsrraUzHK/CXccCt5xM6t7gsgBoJvMNmy2ddpuh6T5xLQpREcI9JVqAihRa33ty
-PMDk4QzPYjKsshYfFqlpRz6Zb3SMUnqjhRlWKyF/QgviNdrK8w1xlx6ix/oVruOP
-P4qSl5vOmGk49CRqWY7IWciD2VHcH04fVfjhwPsh4Dv7uTaMLHwyGvJ8XFjK6yef
-aiSODBsnb+bqKe8jWssCAwEAAaMNMAswCQYDVR0TBAIwADANBgkqhkiG9w0BAQsF
-AAOCAQEAWOjsjh9NxJ5Lu5bGsNQecxheCtkv6WdTB4O5+z60qVNxQZrwQkPbnTQm
-yAfTxR/VubLGqJ3mYU5YsqKJ2Z9Cqvc3wIPXLkruv5A1RwDv0eb0R/UE5nF/nKQR
-B3yn9AYVrzu5kNlpFr/hK6brqfznMKPT6K4n9jMdUg9Bek9b2j8vG7L/Jgona43K
-e7Ip6lACOP0q5PQvrKIkkdfh5QbZjrCQnUVRsScDh/qjWS01rYUp9z7rfuEuQNt5
-TPNMhvmH+podlP9zgONDdwYsjdP59+pFIRebf3Qlkd2nNk6AfL4oFpacr8x7EuHx
-QJZ5MqEauEhsXVSP+gFGywjSsYHMHg==
-2e9046aba2e95ed07efb655f6f50880ef686e531MIICsjCCAZqgAwIBAgIJAJhvbn1Aal/aMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV
-BAMMBnVidW50dTAeFw0xNzEyMjkxNjI3NDhaFw0yNzEyMjcxNjI3NDhaMBExDzAN
-BgNVBAMMBnVidW50dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAObY
-RaZaguRBRowjJraDM8A7VpcWi+aIzYc0003gmz1j+/31pQ7QX12cCFAnyo+6bFlQ
-Or8/Qda24yIAsaaj96Z3iA4bWDUARLUD8N6gftI8sZwf1JRW6kgwYn3DiLdhDeKd
-8LsrraUzHK/CXccCt5xM6t7gsgBoJvMNmy2ddpuh6T5xLQpREcI9JVqAihRa33ty
-PMDk4QzPYjKsshYfFqlpRz6Zb3SMUnqjhRlWKyF/QgviNdrK8w1xlx6ix/oVruOP
-P4qSl5vOmGk49CRqWY7IWciD2VHcH04fVfjhwPsh4Dv7uTaMLHwyGvJ8XFjK6yef
-aiSODBsnb+bqKe8jWssCAwEAAaMNMAswCQYDVR0TBAIwADANBgkqhkiG9w0BAQsF
-AAOCAQEAWOjsjh9NxJ5Lu5bGsNQecxheCtkv6WdTB4O5+z60qVNxQZrwQkPbnTQm
-yAfTxR/VubLGqJ3mYU5YsqKJ2Z9Cqvc3wIPXLkruv5A1RwDv0eb0R/UE5nF/nKQR
-B3yn9AYVrzu5kNlpFr/hK6brqfznMKPT6K4n9jMdUg9Bek9b2j8vG7L/Jgona43K
-e7Ip6lACOP0q5PQvrKIkkdfh5QbZjrCQnUVRsScDh/qjWS01rYUp9z7rfuEuQNt5
-TPNMhvmH+podlP9zgONDdwYsjdP59+pFIRebf3Qlkd2nNk6AfL4oFpacr8x7EuHx
-QJZ5MqEauEhsXVSP+gFGywjSsYHMHg==
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DNT82s99BhXBIvewrpNSnBnuACmZFAKg7Ze+rZmflcQ=
+
+
+
+ gDvJjFd221rI7Y6JT6IFNRr9L1JVQgXiOSx62zfy0Qx7wZTjx1ngs+eOcRloHEdIKBd/BCDQGl10VakEmmzB1OYcuR2V5mq7IdR+lIJb+eoKO1dhc6IK+F2vfWCUxphYUDbfWBE0U06YvSI4di2j0CISMXUbbNiO8DeFWI6/NYuiRimONpRomjwz1X+nR1Aaw58A8hrqiYKMZzMDHL2wJ7haK5ZKv3lWtACpSMYcdNXAzo2le9T0IyZjNUlkGtgHH2UyjDUL1OcZnvMSpd3lHFc6HkUfkbrGmJecNJWZyXT+7BH2HxaYFW0jQEYaTw7vyYatYf0HTNqfcN4VePU7Ww==
+
+ 2e9046aba2e95ed07efb655f6f50880ef686e531
+
+
+
+
+
+ 2e9046aba2e95ed07efb655f6f50880ef686e531
+
+ MIICsjCCAZqgAwIBAgIJAJhvbn1Aal/aMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV
+ BAMMBnVidW50dTAeFw0xNzEyMjkxNjI3NDhaFw0yNzEyMjcxNjI3NDhaMBExDzAN
+ BgNVBAMMBnVidW50dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAObY
+ RaZaguRBRowjJraDM8A7VpcWi+aIzYc0003gmz1j+/31pQ7QX12cCFAnyo+6bFlQ
+ Or8/Qda24yIAsaaj96Z3iA4bWDUARLUD8N6gftI8sZwf1JRW6kgwYn3DiLdhDeKd
+ 8LsrraUzHK/CXccCt5xM6t7gsgBoJvMNmy2ddpuh6T5xLQpREcI9JVqAihRa33ty
+ PMDk4QzPYjKsshYfFqlpRz6Zb3SMUnqjhRlWKyF/QgviNdrK8w1xlx6ix/oVruOP
+ P4qSl5vOmGk49CRqWY7IWciD2VHcH04fVfjhwPsh4Dv7uTaMLHwyGvJ8XFjK6yef
+ aiSODBsnb+bqKe8jWssCAwEAAaMNMAswCQYDVR0TBAIwADANBgkqhkiG9w0BAQsF
+ AAOCAQEAWOjsjh9NxJ5Lu5bGsNQecxheCtkv6WdTB4O5+z60qVNxQZrwQkPbnTQm
+ yAfTxR/VubLGqJ3mYU5YsqKJ2Z9Cqvc3wIPXLkruv5A1RwDv0eb0R/UE5nF/nKQR
+ B3yn9AYVrzu5kNlpFr/hK6brqfznMKPT6K4n9jMdUg9Bek9b2j8vG7L/Jgona43K
+ e7Ip6lACOP0q5PQvrKIkkdfh5QbZjrCQnUVRsScDh/qjWS01rYUp9z7rfuEuQNt5
+ TPNMhvmH+podlP9zgONDdwYsjdP59+pFIRebf3Qlkd2nNk6AfL4oFpacr8x7EuHx
+ QJZ5MqEauEhsXVSP+gFGywjSsYHMHg==
+
+
+
+
+
+
+ 2e9046aba2e95ed07efb655f6f50880ef686e531
+
+ MIICsjCCAZqgAwIBAgIJAJhvbn1Aal/aMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV
+ BAMMBnVidW50dTAeFw0xNzEyMjkxNjI3NDhaFw0yNzEyMjcxNjI3NDhaMBExDzAN
+ BgNVBAMMBnVidW50dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAObY
+ RaZaguRBRowjJraDM8A7VpcWi+aIzYc0003gmz1j+/31pQ7QX12cCFAnyo+6bFlQ
+ Or8/Qda24yIAsaaj96Z3iA4bWDUARLUD8N6gftI8sZwf1JRW6kgwYn3DiLdhDeKd
+ 8LsrraUzHK/CXccCt5xM6t7gsgBoJvMNmy2ddpuh6T5xLQpREcI9JVqAihRa33ty
+ PMDk4QzPYjKsshYfFqlpRz6Zb3SMUnqjhRlWKyF/QgviNdrK8w1xlx6ix/oVruOP
+ P4qSl5vOmGk49CRqWY7IWciD2VHcH04fVfjhwPsh4Dv7uTaMLHwyGvJ8XFjK6yef
+ aiSODBsnb+bqKe8jWssCAwEAAaMNMAswCQYDVR0TBAIwADANBgkqhkiG9w0BAQsF
+ AAOCAQEAWOjsjh9NxJ5Lu5bGsNQecxheCtkv6WdTB4O5+z60qVNxQZrwQkPbnTQm
+ yAfTxR/VubLGqJ3mYU5YsqKJ2Z9Cqvc3wIPXLkruv5A1RwDv0eb0R/UE5nF/nKQR
+ B3yn9AYVrzu5kNlpFr/hK6brqfznMKPT6K4n9jMdUg9Bek9b2j8vG7L/Jgona43K
+ e7Ip6lACOP0q5PQvrKIkkdfh5QbZjrCQnUVRsScDh/qjWS01rYUp9z7rfuEuQNt5
+ TPNMhvmH+podlP9zgONDdwYsjdP59+pFIRebf3Qlkd2nNk6AfL4oFpacr8x7EuHx
+ QJZ5MqEauEhsXVSP+gFGywjSsYHMHg==
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/files/digid/metadata_with_slo_POST b/tests/files/digid/metadata_with_slo_POST
new file mode 100644
index 0000000..b72ee90
--- /dev/null
+++ b/tests/files/digid/metadata_with_slo_POST
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DNT82s99BhXBIvewrpNSnBnuACmZFAKg7Ze+rZmflcQ=
+
+
+
+ gDvJjFd221rI7Y6JT6IFNRr9L1JVQgXiOSx62zfy0Qx7wZTjx1ngs+eOcRloHEdIKBd/BCDQGl10VakEmmzB1OYcuR2V5mq7IdR+lIJb+eoKO1dhc6IK+F2vfWCUxphYUDbfWBE0U06YvSI4di2j0CISMXUbbNiO8DeFWI6/NYuiRimONpRomjwz1X+nR1Aaw58A8hrqiYKMZzMDHL2wJ7haK5ZKv3lWtACpSMYcdNXAzo2le9T0IyZjNUlkGtgHH2UyjDUL1OcZnvMSpd3lHFc6HkUfkbrGmJecNJWZyXT+7BH2HxaYFW0jQEYaTw7vyYatYf0HTNqfcN4VePU7Ww==
+
+ 2e9046aba2e95ed07efb655f6f50880ef686e531
+
+
+
+
+
+ 2e9046aba2e95ed07efb655f6f50880ef686e531
+
+ MIICsjCCAZqgAwIBAgIJAJhvbn1Aal/aMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV
+ BAMMBnVidW50dTAeFw0xNzEyMjkxNjI3NDhaFw0yNzEyMjcxNjI3NDhaMBExDzAN
+ BgNVBAMMBnVidW50dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAObY
+ RaZaguRBRowjJraDM8A7VpcWi+aIzYc0003gmz1j+/31pQ7QX12cCFAnyo+6bFlQ
+ Or8/Qda24yIAsaaj96Z3iA4bWDUARLUD8N6gftI8sZwf1JRW6kgwYn3DiLdhDeKd
+ 8LsrraUzHK/CXccCt5xM6t7gsgBoJvMNmy2ddpuh6T5xLQpREcI9JVqAihRa33ty
+ PMDk4QzPYjKsshYfFqlpRz6Zb3SMUnqjhRlWKyF/QgviNdrK8w1xlx6ix/oVruOP
+ P4qSl5vOmGk49CRqWY7IWciD2VHcH04fVfjhwPsh4Dv7uTaMLHwyGvJ8XFjK6yef
+ aiSODBsnb+bqKe8jWssCAwEAAaMNMAswCQYDVR0TBAIwADANBgkqhkiG9w0BAQsF
+ AAOCAQEAWOjsjh9NxJ5Lu5bGsNQecxheCtkv6WdTB4O5+z60qVNxQZrwQkPbnTQm
+ yAfTxR/VubLGqJ3mYU5YsqKJ2Z9Cqvc3wIPXLkruv5A1RwDv0eb0R/UE5nF/nKQR
+ B3yn9AYVrzu5kNlpFr/hK6brqfznMKPT6K4n9jMdUg9Bek9b2j8vG7L/Jgona43K
+ e7Ip6lACOP0q5PQvrKIkkdfh5QbZjrCQnUVRsScDh/qjWS01rYUp9z7rfuEuQNt5
+ TPNMhvmH+podlP9zgONDdwYsjdP59+pFIRebf3Qlkd2nNk6AfL4oFpacr8x7EuHx
+ QJZ5MqEauEhsXVSP+gFGywjSsYHMHg==
+
+
+
+
+
+
+ 2e9046aba2e95ed07efb655f6f50880ef686e531
+
+ MIICsjCCAZqgAwIBAgIJAJhvbn1Aal/aMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV
+ BAMMBnVidW50dTAeFw0xNzEyMjkxNjI3NDhaFw0yNzEyMjcxNjI3NDhaMBExDzAN
+ BgNVBAMMBnVidW50dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAObY
+ RaZaguRBRowjJraDM8A7VpcWi+aIzYc0003gmz1j+/31pQ7QX12cCFAnyo+6bFlQ
+ Or8/Qda24yIAsaaj96Z3iA4bWDUARLUD8N6gftI8sZwf1JRW6kgwYn3DiLdhDeKd
+ 8LsrraUzHK/CXccCt5xM6t7gsgBoJvMNmy2ddpuh6T5xLQpREcI9JVqAihRa33ty
+ PMDk4QzPYjKsshYfFqlpRz6Zb3SMUnqjhRlWKyF/QgviNdrK8w1xlx6ix/oVruOP
+ P4qSl5vOmGk49CRqWY7IWciD2VHcH04fVfjhwPsh4Dv7uTaMLHwyGvJ8XFjK6yef
+ aiSODBsnb+bqKe8jWssCAwEAAaMNMAswCQYDVR0TBAIwADANBgkqhkiG9w0BAQsF
+ AAOCAQEAWOjsjh9NxJ5Lu5bGsNQecxheCtkv6WdTB4O5+z60qVNxQZrwQkPbnTQm
+ yAfTxR/VubLGqJ3mYU5YsqKJ2Z9Cqvc3wIPXLkruv5A1RwDv0eb0R/UE5nF/nKQR
+ B3yn9AYVrzu5kNlpFr/hK6brqfznMKPT6K4n9jMdUg9Bek9b2j8vG7L/Jgona43K
+ e7Ip6lACOP0q5PQvrKIkkdfh5QbZjrCQnUVRsScDh/qjWS01rYUp9z7rfuEuQNt5
+ TPNMhvmH+podlP9zgONDdwYsjdP59+pFIRebf3Qlkd2nNk6AfL4oFpacr8x7EuHx
+ QJZ5MqEauEhsXVSP+gFGywjSsYHMHg==
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/files/digid/metadata_with_slo_POST_2 b/tests/files/digid/metadata_with_slo_POST_2
new file mode 100644
index 0000000..b567449
--- /dev/null
+++ b/tests/files/digid/metadata_with_slo_POST_2
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DNT82s99BhXBIvewrpNSnBnuACmZFAKg7Ze+rZmflcQ=
+
+
+
+ gDvJjFd221rI7Y6JT6IFNRr9L1JVQgXiOSx62zfy0Qx7wZTjx1ngs+eOcRloHEdIKBd/BCDQGl10VakEmmzB1OYcuR2V5mq7IdR+lIJb+eoKO1dhc6IK+F2vfWCUxphYUDbfWBE0U06YvSI4di2j0CISMXUbbNiO8DeFWI6/NYuiRimONpRomjwz1X+nR1Aaw58A8hrqiYKMZzMDHL2wJ7haK5ZKv3lWtACpSMYcdNXAzo2le9T0IyZjNUlkGtgHH2UyjDUL1OcZnvMSpd3lHFc6HkUfkbrGmJecNJWZyXT+7BH2HxaYFW0jQEYaTw7vyYatYf0HTNqfcN4VePU7Ww==
+
+ 2e9046aba2e95ed07efb655f6f50880ef686e531
+
+
+
+
+
+ 2e9046aba2e95ed07efb655f6f50880ef686e531
+
+ MIICsjCCAZqgAwIBAgIJAJhvbn1Aal/aMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV
+ BAMMBnVidW50dTAeFw0xNzEyMjkxNjI3NDhaFw0yNzEyMjcxNjI3NDhaMBExDzAN
+ BgNVBAMMBnVidW50dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAObY
+ RaZaguRBRowjJraDM8A7VpcWi+aIzYc0003gmz1j+/31pQ7QX12cCFAnyo+6bFlQ
+ Or8/Qda24yIAsaaj96Z3iA4bWDUARLUD8N6gftI8sZwf1JRW6kgwYn3DiLdhDeKd
+ 8LsrraUzHK/CXccCt5xM6t7gsgBoJvMNmy2ddpuh6T5xLQpREcI9JVqAihRa33ty
+ PMDk4QzPYjKsshYfFqlpRz6Zb3SMUnqjhRlWKyF/QgviNdrK8w1xlx6ix/oVruOP
+ P4qSl5vOmGk49CRqWY7IWciD2VHcH04fVfjhwPsh4Dv7uTaMLHwyGvJ8XFjK6yef
+ aiSODBsnb+bqKe8jWssCAwEAAaMNMAswCQYDVR0TBAIwADANBgkqhkiG9w0BAQsF
+ AAOCAQEAWOjsjh9NxJ5Lu5bGsNQecxheCtkv6WdTB4O5+z60qVNxQZrwQkPbnTQm
+ yAfTxR/VubLGqJ3mYU5YsqKJ2Z9Cqvc3wIPXLkruv5A1RwDv0eb0R/UE5nF/nKQR
+ B3yn9AYVrzu5kNlpFr/hK6brqfznMKPT6K4n9jMdUg9Bek9b2j8vG7L/Jgona43K
+ e7Ip6lACOP0q5PQvrKIkkdfh5QbZjrCQnUVRsScDh/qjWS01rYUp9z7rfuEuQNt5
+ TPNMhvmH+podlP9zgONDdwYsjdP59+pFIRebf3Qlkd2nNk6AfL4oFpacr8x7EuHx
+ QJZ5MqEauEhsXVSP+gFGywjSsYHMHg==
+
+
+
+
+
+
+ 2e9046aba2e95ed07efb655f6f50880ef686e531
+
+ MIICsjCCAZqgAwIBAgIJAJhvbn1Aal/aMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV
+ BAMMBnVidW50dTAeFw0xNzEyMjkxNjI3NDhaFw0yNzEyMjcxNjI3NDhaMBExDzAN
+ BgNVBAMMBnVidW50dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAObY
+ RaZaguRBRowjJraDM8A7VpcWi+aIzYc0003gmz1j+/31pQ7QX12cCFAnyo+6bFlQ
+ Or8/Qda24yIAsaaj96Z3iA4bWDUARLUD8N6gftI8sZwf1JRW6kgwYn3DiLdhDeKd
+ 8LsrraUzHK/CXccCt5xM6t7gsgBoJvMNmy2ddpuh6T5xLQpREcI9JVqAihRa33ty
+ PMDk4QzPYjKsshYfFqlpRz6Zb3SMUnqjhRlWKyF/QgviNdrK8w1xlx6ix/oVruOP
+ P4qSl5vOmGk49CRqWY7IWciD2VHcH04fVfjhwPsh4Dv7uTaMLHwyGvJ8XFjK6yef
+ aiSODBsnb+bqKe8jWssCAwEAAaMNMAswCQYDVR0TBAIwADANBgkqhkiG9w0BAQsF
+ AAOCAQEAWOjsjh9NxJ5Lu5bGsNQecxheCtkv6WdTB4O5+z60qVNxQZrwQkPbnTQm
+ yAfTxR/VubLGqJ3mYU5YsqKJ2Z9Cqvc3wIPXLkruv5A1RwDv0eb0R/UE5nF/nKQR
+ B3yn9AYVrzu5kNlpFr/hK6brqfznMKPT6K4n9jMdUg9Bek9b2j8vG7L/Jgona43K
+ e7Ip6lACOP0q5PQvrKIkkdfh5QbZjrCQnUVRsScDh/qjWS01rYUp9z7rfuEuQNt5
+ TPNMhvmH+podlP9zgONDdwYsjdP59+pFIRebf3Qlkd2nNk6AfL4oFpacr8x7EuHx
+ QJZ5MqEauEhsXVSP+gFGywjSsYHMHg==
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/test_commands.py b/tests/test_commands.py
new file mode 100644
index 0000000..144949d
--- /dev/null
+++ b/tests/test_commands.py
@@ -0,0 +1,82 @@
+from io import StringIO
+from unittest.mock import patch
+
+from django.core.management import CommandError, call_command
+from django.test import TestCase
+
+import pytest
+
+from digid_eherkenning.models import DigidConfiguration, EherkenningConfiguration
+
+from .conftest import DIGID_TEST_METADATA_FILE_SLO_POST, EHERKENNING_TEST_METADATA_FILE
+
+
+@pytest.mark.usefixtures("digid_config_defaults", "temp_private_root")
+class UpdateStoredMetadataTests(TestCase):
+ @patch(
+ "onelogin.saml2.idp_metadata_parser.OneLogin_Saml2_IdPMetadataParser.get_metadata"
+ )
+ def test_command_triggers_xml_fetching_when_digid(self, get_metadata):
+ output = StringIO()
+ config = DigidConfiguration.get_solo()
+
+ with DIGID_TEST_METADATA_FILE_SLO_POST.open("rb") as metadata_file:
+ metadata_content = metadata_file.read().decode("utf-8")
+ get_metadata.return_value = metadata_content
+ config.metadata_file_source = (
+ "https://was-preprod1.digid.nl/saml/idp/metadata"
+ )
+ config.save()
+
+ self.assertEqual(get_metadata.call_count, 1)
+
+ get_metadata.reset_mock()
+
+ call_command("update_stored_metadata", "digid", stdout=output)
+
+ self.assertEqual(get_metadata.call_count, 1)
+ self.assertEqual(output.getvalue(), "Update was successful\n")
+
+ @patch(
+ "onelogin.saml2.idp_metadata_parser.OneLogin_Saml2_IdPMetadataParser.get_metadata"
+ )
+ def test_command_triggers_xml_fetching_when_eherkenning(self, get_metadata):
+ output = StringIO()
+ config = EherkenningConfiguration.get_solo()
+
+ with EHERKENNING_TEST_METADATA_FILE.open("rb") as metadata_file:
+ metadata_content = metadata_file.read().decode("utf-8")
+ get_metadata.return_value = metadata_content
+ config.metadata_file_source = (
+ "https://eh01.staging.iwelcome.nl/broker/sso/1.13"
+ )
+ config.save()
+
+ self.assertEqual(get_metadata.call_count, 1)
+
+ get_metadata.reset_mock()
+
+ call_command("update_stored_metadata", "eherkenning", stdout=output)
+
+ self.assertEqual(get_metadata.call_count, 1)
+ self.assertEqual(output.getvalue(), "Update was successful\n")
+
+ def test_command_fails_when_no_argument_provided(self):
+ try:
+ call_command("update_stored_metadata")
+ except CommandError as e:
+ error_message = str(e)
+
+ self.assertEqual(
+ error_message,
+ "Error: the following arguments are required: config_model",
+ )
+
+ def test_command_fails_when_no_metadata_file_source(self):
+ output = StringIO()
+ call_command("update_stored_metadata", "digid", stdout=output)
+
+ self.assertEqual(
+ output.getvalue(),
+ "Update failed, no metadata file source found\n",
+ )
diff --git a/tests/test_models.py b/tests/test_models.py
new file mode 100644
index 0000000..b5c3ed2
--- /dev/null
+++ b/tests/test_models.py
@@ -0,0 +1,132 @@
+from unittest.mock import patch
+
+from django.core.exceptions import ValidationError
+from django.test import TestCase
+from django.utils.translation import gettext as _
+
+import pytest
+
+from digid_eherkenning.models import DigidConfiguration, EherkenningConfiguration
+
+from .conftest import (
+ DIGID_TEST_METADATA_FILE_SLO_POST,
+ DIGID_TEST_METADATA_FILE_SLO_POST_2,
+ EHERKENNING_TEST_METADATA_FILE,
+)
+
+
+@pytest.mark.usefixtures("digid_config_defaults", "temp_private_root")
+class BaseModelTests(TestCase):
+ @patch(
+ "onelogin.saml2.idp_metadata_parser.OneLogin_Saml2_IdPMetadataParser.get_metadata"
+ )
+ def test_fields_are_populated_on_digid_save(self, get_matadata):
+ config = DigidConfiguration.get_solo()
+
+ with DIGID_TEST_METADATA_FILE_SLO_POST.open("rb") as metadata_file:
+ metadata_content = metadata_file.read().decode("utf-8")
+ get_matadata.return_value = metadata_content
+ config.metadata_file_source = (
+ "https://was-preprod1.digid.nl/saml/idp/metadata/with-slo"
+ )
+ config.save()
+
+ self.assertTrue(get_matadata.called_once())
+ self.assertEqual(
+ config.idp_metadata_file.read().decode("utf-8"), metadata_content
+ )
+ self.assertEqual(
+ config.idp_service_entity_id,
+ "https://was-preprod1.digid.nl/saml/idp/metadata/with-slo",
+ )
+
+ @patch(
+ "onelogin.saml2.idp_metadata_parser.OneLogin_Saml2_IdPMetadataParser.get_metadata"
+ )
+ def test_fields_are_populated_on_eherkennig_save(self, get_matadata):
+ config = EherkenningConfiguration.get_solo()
+
+ with EHERKENNING_TEST_METADATA_FILE.open("rb") as metadata_file:
+ metadata_content = metadata_file.read().decode("utf-8")
+ get_matadata.return_value = metadata_content
+ config.metadata_file_source = (
+ "https://eh01.staging.iwelcome.nl/broker/sso/1.13"
+ )
+ config.save()
+
+ self.assertTrue(get_matadata.called_once())
+ self.assertEqual(
+ config.idp_metadata_file.read().decode("utf-8"), metadata_content
+ )
+ self.assertEqual(
+ config.idp_service_entity_id,
+ "https://eh01.staging.iwelcome.nl/broker/sso/1.13",
+ )
+
+ @patch(
+ "onelogin.saml2.idp_metadata_parser.OneLogin_Saml2_IdPMetadataParser.get_metadata"
+ )
+ def test_no_fetching_xml_when_no_file_source_change(self, get_matadata):
+ config = DigidConfiguration.get_solo()
+
+ with DIGID_TEST_METADATA_FILE_SLO_POST.open("rb") as metadata_file:
+ metadata_content = metadata_file.read().decode("utf-8")
+ get_matadata.return_value = metadata_content
+ config.metadata_file_source = (
+ "https://was-preprod1.digid.nl/saml/idp/metadata"
+ )
+ config.save()
+
+ config.organization_name = "test"
+ config.save()
+
+ # Make sure we don't try to fetch and parse again the xml file, since there is no update
+ self.assertTrue(get_matadata.called_once())
+
+ @patch(
+ "onelogin.saml2.idp_metadata_parser.OneLogin_Saml2_IdPMetadataParser.get_metadata"
+ )
+ def test_wrong_xml_format_raises_validation_error(self, get_matadata):
+ config = DigidConfiguration.get_solo()
+
+ with DIGID_TEST_METADATA_FILE_SLO_POST.open("rb") as metadata_file:
+ metadata_content = metadata_file.read().decode("utf-8")
+ get_matadata.return_value = metadata_content
+ config.metadata_file_source = (
+ "https://was-preprod1.digid.nl/saml/idp/metadata"
+ )
+ config.save()
+
+ get_matadata.return_value = "wrong xml format"
+ config.metadata_file_source = "https://example.com"
+
+ with self.assertRaisesMessage(
+ ValidationError,
+ _(
+ "Failed to parse the metadata, got error: Start tag expected, '<' not found, line 1, column 1"
+ ),
+ ):
+ config.save()
+
+ @patch("onelogin.saml2.idp_metadata_parser.OneLogin_Saml2_IdPMetadataParser.parse")
+ @patch(
+ "onelogin.saml2.idp_metadata_parser.OneLogin_Saml2_IdPMetadataParser.get_metadata"
+ )
+ def test_no_idp_in_xml_raises_validation_error(self, get_matadata, parse):
+ config = DigidConfiguration.get_solo()
+
+ with DIGID_TEST_METADATA_FILE_SLO_POST_2.open("rb") as metadata_file:
+ metadata_content = metadata_file.read().decode("utf-8")
+ get_matadata.return_value = metadata_content
+ config.metadata_file_source = (
+ "https://was-preprod1.digid.nl/saml/idp/metadata"
+ )
+ parse.return_value = {"test_no_idp": ""}
+
+ with self.assertRaisesMessage(
+ ValidationError,
+ _(
+ "Could not find any identity provider information in the metadata at the provided URL."
+ ),
+ ):
+ config.save()