1010"""
1111
1212import urllib2
13+
14+ from copy import deepcopy
1315from defusedxml .lxml import fromstring
1416
1517from onelogin .saml2 .constants import OneLogin_Saml2_Constants
@@ -51,7 +53,7 @@ def get_metadata(url):
5153 return xml
5254
5355 @staticmethod
54- def parse_remote (url ):
56+ def parse_remote (url , ** kwargs ):
5557 """
5658 Get the metadata XML from the provided URL and parse it, returning a dict with extracted data
5759
@@ -62,22 +64,41 @@ def parse_remote(url):
6264 :rtype: dict
6365 """
6466 idp_metadata = OneLogin_Saml2_IdPMetadataParser .get_metadata (url )
65- return OneLogin_Saml2_IdPMetadataParser .parse (idp_metadata )
67+ return OneLogin_Saml2_IdPMetadataParser .parse (idp_metadata , ** kwargs )
6668
6769 @staticmethod
68- def parse (idp_metadata ):
70+ def parse (
71+ idp_metadata ,
72+ required_sso_binding = OneLogin_Saml2_Constants .BINDING_HTTP_REDIRECT ,
73+ required_slo_binding = OneLogin_Saml2_Constants .BINDING_HTTP_REDIRECT ):
6974 """
70- Parse the Identity Provider metadata and returns a dict with extracted data
71- If there are multiple IDPSSODescriptor it will only parse the first
75+ Parse the Identity Provider metadata and return a dict with extracted data.
76+
77+ If there are multiple <IDPSSODescriptor> tags, parse only the first.
78+
79+ Parse only those SSO endpoints with the same binding as given by
80+ the `required_sso_binding` parameter.
81+
82+ Parse only those SLO endpoints with the same binding as given by
83+ the `required_slo_binding` parameter.
84+
85+ If the metadata specifies multiple SSO endpoints with the required
86+ binding, extract only the first (the same holds true for SLO
87+ endpoints).
7288
7389 :param idp_metadata: XML of the Identity Provider Metadata.
7490 :type idp_metadata: string
7591
76- :param url: If true and the URL is HTTPs, the cert of the domain is checked.
77- :type url: bool
92+ :param required_sso_binding: Parse only POST or REDIRECT SSO endpoints.
93+ :type required_sso_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
94+ or OneLogin_Saml2_Constants.BINDING_HTTP_POST
95+
96+ :param required_slo_binding: Parse only POST or REDIRECT SLO endpoints.
97+ :type required_slo_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
98+ or OneLogin_Saml2_Constants.BINDING_HTTP_POST
7899
79100 :returns: settings dict with extracted data
80- :rtype: string
101+ :rtype: dict
81102 """
82103 data = {}
83104
@@ -100,11 +121,18 @@ def parse(idp_metadata):
100121 if len (name_id_format_nodes ) > 0 :
101122 idp_name_id_format = name_id_format_nodes [0 ].text
102123
103- sso_nodes = OneLogin_Saml2_Utils .query (idp_descriptor_node , "./md:SingleSignOnService[@Binding='%s']" % OneLogin_Saml2_Constants .BINDING_HTTP_REDIRECT )
124+ sso_nodes = OneLogin_Saml2_Utils .query (
125+ idp_descriptor_node ,
126+ "./md:SingleSignOnService[@Binding='%s']" % required_sso_binding
127+ )
128+
104129 if len (sso_nodes ) > 0 :
105130 idp_sso_url = sso_nodes [0 ].get ('Location' , None )
106131
107- slo_nodes = OneLogin_Saml2_Utils .query (idp_descriptor_node , "./md:SingleLogoutService[@Binding='%s']" % OneLogin_Saml2_Constants .BINDING_HTTP_REDIRECT )
132+ slo_nodes = OneLogin_Saml2_Utils .query (
133+ idp_descriptor_node ,
134+ "./md:SingleLogoutService[@Binding='%s']" % required_slo_binding
135+ )
108136 if len (slo_nodes ) > 0 :
109137 idp_slo_url = slo_nodes [0 ].get ('Location' , None )
110138
@@ -116,12 +144,17 @@ def parse(idp_metadata):
116144
117145 if idp_entity_id is not None :
118146 data ['idp' ]['entityId' ] = idp_entity_id
147+
119148 if idp_sso_url is not None :
120149 data ['idp' ]['singleSignOnService' ] = {}
121150 data ['idp' ]['singleSignOnService' ]['url' ] = idp_sso_url
151+ data ['idp' ]['singleSignOnService' ]['binding' ] = required_sso_binding
152+
122153 if idp_slo_url is not None :
123154 data ['idp' ]['singleLogoutService' ] = {}
124155 data ['idp' ]['singleLogoutService' ]['url' ] = idp_slo_url
156+ data ['idp' ]['singleLogoutService' ]['binding' ] = required_slo_binding
157+
125158 if idp_x509_cert is not None :
126159 data ['idp' ]['x509cert' ] = idp_x509_cert
127160
@@ -150,6 +183,34 @@ def merge_settings(settings, new_metadata_settings):
150183 :returns: merged settings
151184 :rtype: dict
152185 """
153- result_settings = settings .copy ()
154- result_settings .update (new_metadata_settings )
186+ for d in (settings , new_metadata_settings ):
187+ if not isinstance (d , dict ):
188+ raise TypeError ('Both arguments must be dictionaries.' )
189+
190+ # Guarantee to not modify original data (`settings.copy()` would not
191+ # be sufficient, as it's just a shallow copy).
192+ result_settings = deepcopy (settings )
193+ # Merge `new_metadata_settings` into `result_settings`.
194+ dict_deep_merge (result_settings , new_metadata_settings )
155195 return result_settings
196+
197+
198+ def dict_deep_merge (a , b , path = None ):
199+ """Deep-merge dictionary `b` into dictionary `a`.
200+ Kudos to http://stackoverflow.com/a/7205107/145400
201+ """
202+ if path is None :
203+ path = []
204+ for key in b :
205+ if key in a :
206+ if isinstance (a [key ], dict ) and isinstance (b [key ], dict ):
207+ dict_deep_merge (a [key ], b [key ], path + [str (key )])
208+ elif a [key ] == b [key ]:
209+ # Key conflict, but equal value.
210+ pass
211+ else :
212+ # Key/value conflict. Prioritize b over a.
213+ a [key ] = b [key ]
214+ else :
215+ a [key ] = b [key ]
216+ return a
0 commit comments