Skip to content

Commit

Permalink
[#45] Started with auto populating url and metadata file fields
Browse files Browse the repository at this point in the history
  • Loading branch information
vaszig committed Oct 11, 2023
1 parent e919dd6 commit b55f345
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 5 deletions.
6 changes: 4 additions & 2 deletions digid_eherkenning/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ class DigidConfigurationAdmin(PrivateMediaMixin, SingletonModelAdmin):
_("Identity provider"),
{
"fields": (
"idp_metadata_file",
"metadata_file_source",
"idp_service_entity_id",
"idp_metadata_file",
),
},
),
Expand Down Expand Up @@ -86,8 +87,9 @@ class EherkenningConfigurationAdmin(PrivateMediaMixin, SingletonModelAdmin):
_("Identity provider"),
{
"fields": (
"idp_metadata_file",
"metadata_file_source",
"idp_service_entity_id",
"idp_metadata_file",
),
},
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Generated by Django 4.2.6 on 2023-10-11 12:49

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 populatedby the retrieved metadata XML file.",
null=True,
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 by the retrieved metadata XML file.",
max_length=255,
null=True,
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 populatedby the retrieved metadata XML file.",
null=True,
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 by the retrieved metadata XML file.",
max_length=255,
null=True,
verbose_name="identity provider service entity ID",
),
),
]
76 changes: 73 additions & 3 deletions digid_eherkenning/models/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from django.core.cache import cache
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
Expand All @@ -29,17 +33,31 @@ class BaseConfiguration(SingletonModel):
)
idp_metadata_file = PrivateMediaFileField(
_("identity provider metadata"),
blank=False,
help_text=_("The metadata file of the identity provider."),
blank=True,
null=True,
help_text=_(
"The metadata file of the identity provider. This is auto populated"
"by the retrieved metadata XML file."
),
)
idp_service_entity_id = models.CharField(
_("identity provider service entity ID"),
max_length=255,
blank=False,
blank=True,
null=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 by the retrieved metadata XML file."
),
)
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(
Expand Down Expand Up @@ -166,7 +184,59 @@ class Meta:
def __str__(self):
return force_str(self._meta.verbose_name)

def populate_xml_fields(self, urls, xml):
"""
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 fetch_xml_metadata(self):
"""
Retrieves the xml metadata from the provided url (self.metadata_file_source)
:return a string of the xml metadata.
"""
return OneLogin_Saml2_IdPMetadataParser.get_metadata(self.metadata_file_source)

def parse_data_from_xml_source(self):
"""
Parses the xml metadata
:return a tuple of a dictionary with the useful urls and the xml string itself.
"""
urls = {}

xml = self.fetch_xml_metadata()
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,
)

# TODO
# Handle the case that we have multiple entityIds and consider caching
# the results as well or add a management command for updating them
if idp := parsed_idp_metadata.get("idp"):
urls = {
"entityId": idp.get("entityId"),
"sso_url": idp.get("singleSignOnService", {}).get("url"),
"slo_url": idp.get("singleLogoutService", {}).get("url"),
}

return (urls, xml)

def save(self, *args, **kwargs):
if self.pk and self.metadata_file_source:
urls, xml = self.parse_data_from_xml_source()

if urls and xml:
self.populate_xml_fields(urls, xml)
else:
raise ValidationError(_("The provided url is not valid"))

Check warning on line 238 in digid_eherkenning/models/base.py

View check run for this annotation

Codecov / codecov/patch

digid_eherkenning/models/base.py#L238

Added line #L238 was not covered by tests

if self.base_url.endswith("/"):
self.base_url = self.base_url[:-1]
super().save(*args, **kwargs)
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@


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_KEY_FILE = BASE_DIR / "files" / "snakeoil-cert" / "ssl-cert-snakeoil.key"
DIGID_TEST_CERTIFICATE_FILE = (
BASE_DIR / "files" / "snakeoil-cert" / "ssl-cert-snakeoil.pem"
Expand Down
85 changes: 85 additions & 0 deletions tests/files/digid/metadata_with_slo_POST
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#"
ID="_a1d008fa9c840e932100ed323d460eb02b0a2f8d"
entityID="https://was-preprod1.digid.nl/saml/idp/metadata">
<ds:Signature>
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
<ds:Reference URI="#_a1d008fa9c840e932100ed323d460eb02b0a2f8d">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
<ec:InclusiveNamespaces PrefixList="ds saml samlp xs" />
</ds:Transform>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
<ds:DigestValue>DNT82s99BhXBIvewrpNSnBnuACmZFAKg7Ze+rZmflcQ=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>
gDvJjFd221rI7Y6JT6IFNRr9L1JVQgXiOSx62zfy0Qx7wZTjx1ngs+eOcRloHEdIKBd/BCDQGl10VakEmmzB1OYcuR2V5mq7IdR+lIJb+eoKO1dhc6IK+F2vfWCUxphYUDbfWBE0U06YvSI4di2j0CISMXUbbNiO8DeFWI6/NYuiRimONpRomjwz1X+nR1Aaw58A8hrqiYKMZzMDHL2wJ7haK5ZKv3lWtACpSMYcdNXAzo2le9T0IyZjNUlkGtgHH2UyjDUL1OcZnvMSpd3lHFc6HkUfkbrGmJecNJWZyXT+7BH2HxaYFW0jQEYaTw7vyYatYf0HTNqfcN4VePU7Ww==</ds:SignatureValue>
<ds:KeyInfo>
<ds:KeyName>2e9046aba2e95ed07efb655f6f50880ef686e531</ds:KeyName>
</ds:KeyInfo>
</ds:Signature>
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo>
<ds:KeyName>2e9046aba2e95ed07efb655f6f50880ef686e531</ds:KeyName>
<ds:X509Data>
<ds:X509Certificate>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==
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:KeyDescriptor use="encryption">
<ds:KeyInfo>
<ds:KeyName>2e9046aba2e95ed07efb655f6f50880ef686e531</ds:KeyName>
<ds:X509Data>
<ds:X509Certificate>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==
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="https://was-preprod1.digid.nl/saml/idp/resolve_artifact" index="0" />
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://preprod1.digid.nl/saml/idp/request_logout" />
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="https://preprod1.digid.nl/saml/idp/request_logout" />
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://preprod1.digid.nl/saml/idp/request_authentication" />
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="https://preprod1.digid.nl/saml/idp/request_authentication" />
</md:IDPSSODescriptor>
</md:EntityDescriptor>
33 changes: 33 additions & 0 deletions tests/test_auto_populate_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from django.test import TestCase

import pytest
from unittest.mock import patch

from digid_eherkenning.models import DigidConfiguration

from .conftest import DIGID_TEST_METADATA_FILE_SLO_POST


@pytest.mark.usefixtures("digid_config_defaults", "temp_private_root")
class DigidAutoPopulateFieldsTests(TestCase):
@patch(
"onelogin.saml2.idp_metadata_parser.OneLogin_Saml2_IdPMetadataParser.get_metadata"
)
def test_fields_are_populated(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()

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",
)

0 comments on commit b55f345

Please sign in to comment.