66Metadata class of OneLogin's Python Toolkit.
77"""
88
9+
10+ from copy import deepcopy
11+
12+
913try :
1014 import urllib .request as urllib2
1115except 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
0 commit comments