Skip to content

Commit 94cd30d

Browse files
committed
Merge branch 'jgehrcke-jp/fixparser2'. IdP metadata parser/merger adjustments + test compatibility + code style
2 parents c512616 + e759831 commit 94cd30d

13 files changed

Lines changed: 894 additions & 120 deletions

src/onelogin/saml2/idp_metadata_parser.py

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
Metadata class of OneLogin's Python Toolkit.
77
"""
88

9+
10+
from copy import deepcopy
11+
12+
913
try:
1014
import urllib.request as urllib2
1115
except ImportError:
@@ -48,7 +52,7 @@ def get_metadata(url):
4852
return xml
4953

5054
@staticmethod
51-
def parse_remote(url):
55+
def parse_remote(url, **kwargs):
5256
"""
5357
Get the metadata XML from the provided URL and parse it, returning a dict with extracted data
5458
:param url: Url where the XML of the Identity Provider Metadata is published.
@@ -57,19 +61,41 @@ def parse_remote(url):
5761
:rtype: dict
5862
"""
5963
idp_metadata = OneLogin_Saml2_IdPMetadataParser.get_metadata(url)
60-
return OneLogin_Saml2_IdPMetadataParser.parse(idp_metadata)
64+
return OneLogin_Saml2_IdPMetadataParser.parse(idp_metadata, **kwargs)
6165

6266
@staticmethod
63-
def parse(idp_metadata):
67+
def parse(
68+
idp_metadata,
69+
required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT,
70+
required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT):
6471
"""
65-
Parse the Identity Provider metadata and returns a dict with extracted data
66-
If there are multiple IDPSSODescriptor it will only parse the first
72+
Parse the Identity Provider metadata and return a dict with extracted data.
73+
74+
If there are multiple <IDPSSODescriptor> tags, parse only the first.
75+
76+
Parse only those SSO endpoints with the same binding as given by
77+
the `required_sso_binding` parameter.
78+
79+
Parse only those SLO endpoints with the same binding as given by
80+
the `required_slo_binding` parameter.
81+
82+
If the metadata specifies multiple SSO endpoints with the required
83+
binding, extract only the first (the same holds true for SLO
84+
endpoints).
85+
6786
:param idp_metadata: XML of the Identity Provider Metadata.
6887
:type idp_metadata: string
69-
:param url: If true and the URL is HTTPs, the cert of the domain is checked.
70-
:type url: bool
88+
89+
:param required_sso_binding: Parse only POST or REDIRECT SSO endpoints.
90+
:type required_sso_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
91+
or OneLogin_Saml2_Constants.BINDING_HTTP_POST
92+
93+
:param required_slo_binding: Parse only POST or REDIRECT SLO endpoints.
94+
:type required_slo_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
95+
or OneLogin_Saml2_Constants.BINDING_HTTP_POST
96+
7197
:returns: settings dict with extracted data
72-
:rtype: string
98+
:rtype: dict
7399
"""
74100
data = {}
75101

@@ -92,11 +118,19 @@ def parse(idp_metadata):
92118
if len(name_id_format_nodes) > 0:
93119
idp_name_id_format = name_id_format_nodes[0].text
94120

95-
sso_nodes = OneLogin_Saml2_XML.query(idp_descriptor_node, "./md:SingleSignOnService[@Binding='%s']" % OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT)
121+
sso_nodes = OneLogin_Saml2_XML.query(
122+
idp_descriptor_node,
123+
"./md:SingleSignOnService[@Binding='%s']" % required_sso_binding
124+
)
125+
96126
if len(sso_nodes) > 0:
97127
idp_sso_url = sso_nodes[0].get('Location', None)
98128

99-
slo_nodes = OneLogin_Saml2_XML.query(idp_descriptor_node, "./md:SingleLogoutService[@Binding='%s']" % OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT)
129+
slo_nodes = OneLogin_Saml2_XML.query(
130+
idp_descriptor_node,
131+
"./md:SingleLogoutService[@Binding='%s']" % required_slo_binding
132+
)
133+
100134
if len(slo_nodes) > 0:
101135
idp_slo_url = slo_nodes[0].get('Location', None)
102136

@@ -108,12 +142,17 @@ def parse(idp_metadata):
108142

109143
if idp_entity_id is not None:
110144
data['idp']['entityId'] = idp_entity_id
145+
111146
if idp_sso_url is not None:
112147
data['idp']['singleSignOnService'] = {}
113148
data['idp']['singleSignOnService']['url'] = idp_sso_url
149+
data['idp']['singleSignOnService']['binding'] = required_sso_binding
150+
114151
if idp_slo_url is not None:
115152
data['idp']['singleLogoutService'] = {}
116153
data['idp']['singleLogoutService']['url'] = idp_slo_url
154+
data['idp']['singleLogoutService']['binding'] = required_slo_binding
155+
117156
if idp_x509_cert is not None:
118157
data['idp']['x509cert'] = idp_x509_cert
119158

@@ -139,6 +178,35 @@ def merge_settings(settings, new_metadata_settings):
139178
:returns: merged settings
140179
:rtype: dict
141180
"""
142-
result_settings = settings.copy()
143-
result_settings.update(new_metadata_settings)
181+
for d in (settings, new_metadata_settings):
182+
if not isinstance(d, dict):
183+
raise TypeError('Both arguments must be dictionaries.')
184+
185+
# Guarantee to not modify original data (`settings.copy()` would not
186+
# be sufficient, as it's just a shallow copy).
187+
result_settings = deepcopy(settings)
188+
# Merge `new_metadata_settings` into `result_settings`.
189+
dict_deep_merge(result_settings, new_metadata_settings)
144190
return result_settings
191+
192+
193+
def dict_deep_merge(a, b, path=None):
194+
"""Deep-merge dictionary `b` into dictionary `a`.
195+
196+
Kudos to http://stackoverflow.com/a/7205107/145400
197+
"""
198+
if path is None:
199+
path = []
200+
for key in b:
201+
if key in a:
202+
if isinstance(a[key], dict) and isinstance(b[key], dict):
203+
dict_deep_merge(a[key], b[key], path + [str(key)])
204+
elif a[key] == b[key]:
205+
# Key conflict, but equal value.
206+
pass
207+
else:
208+
# Key/value conflict. Prioritize b over a.
209+
a[key] = b[key]
210+
else:
211+
a[key] = b[key]
212+
return a
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<EntityDescriptor entityID="urn:example:idp" xmlns="urn:oasis:names:tc:SAML:2.0:metadata"> <IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> <KeyDescriptor use="signing"> <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"> <X509Data> <X509Certificate>MIIDPDCCAiQCCQDydJgOlszqbzANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEQMA4GA1UEChMHSmFua3lDbzESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTE0MDMxMjE5NDYzM1oXDTI3MTExOTE5NDYzM1owYDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEDAOBgNVBAoTB0phbmt5Q28xEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMGvJpRTTasRUSPqcbqCG+ZnTAurnu0vVpIG9lzExnh11o/BGmzu7lB+yLHcEdwrKBBmpepDBPCYxpVajvuEhZdKFx/Fdy6j5mH3rrW0Bh/zd36CoUNjbbhHyTjeM7FN2yF3u9lcyubuvOzr3B3gX66IwJlU46+wzcQVhSOlMk2tXR+fIKQExFrOuK9tbX3JIBUqItpI+HnAow509CnM134svw8PTFLkR6/CcMqnDfDK1m993PyoC1Y+N4X9XkhSmEQoAlAHPI5LHrvuujM13nvtoVYvKYoj7ScgumkpWNEvX652LfXOnKYlkB8ZybuxmFfIkzedQrbJsyOhfL03cMECAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAeHwzqwnzGEkxjzSD47imXaTqtYyETZow7XwBc0ZaFS50qRFJUgKTAmKS1xQBP/qHpStsROT35DUxJAE6NY1Kbq3ZbCuhGoSlY0L7VzVT5tpu4EY8+Dq/u2EjRmmhoL7UkskvIZ2n1DdERtd+YUMTeqYl9co43csZwDno/IKomeN5qaPc39IZjikJ+nUC6kPFKeu/3j9rgHNlRtocI6S1FdtFz9OZMQlpr0JbUt2T3xS/YoQJn6coDmJL5GTiiKM6cOe+Ur1VwzS1JEDbSS2TWWhzq8ojLdrotYLGd9JOsoQhElmz+tMfCFQUFLExinPAyy7YHlSiVX13QH2XTu/iQQ==</X509Certificate> </X509Data> </KeyInfo> </KeyDescriptor> <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://idp.example.com/logout"/> <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat> <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat> <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat> <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://idp.example.com"/> <Attribute Name="Email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="E-Mail Address" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"/> <Attribute Name="FirstName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="First Name" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"/> <Attribute Name="LastName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="Last Name" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"/> </IDPSSODescriptor></EntityDescriptor>

0 commit comments

Comments
 (0)