1212from time import gmtime , strftime
1313from datetime import datetime
1414from defusedxml .minidom import parseString
15+ from lxml import etree
1516
1617from onelogin .saml2 .constants import OneLogin_Saml2_Constants
1718from onelogin .saml2 .utils import OneLogin_Saml2_Utils
@@ -27,6 +28,63 @@ class OneLogin_Saml2_Metadata(object):
2728 TIME_VALID = 172800 # 2 days
2829 TIME_CACHED = 604800 # 1 week
2930
31+ @staticmethod
32+ def add_attribute_consuming_service (root , attr_consuming_service ):
33+ """Helper function to add the AttributeConsumingService nodes"""
34+ attrib_index = 1
35+ spso_node = root .find ('{%s}SPSSODescriptor' % OneLogin_Saml2_Constants .NS_MD )
36+
37+ # iterate through all the consuming services listed
38+ for acs in attr_consuming_service :
39+ acs_node = etree .SubElement (spso_node , "{%s}AttributeConsumingService" % OneLogin_Saml2_Constants .NS_MD )
40+ acs_node .set ('index' , str (attrib_index ))
41+ try :
42+ acs_node .set ('isDefault' , str (acs ['isDefault' ]).lower ())
43+ except KeyError :
44+ pass
45+
46+ svc_name = etree .SubElement (acs_node , "{%s}ServiceName" % OneLogin_Saml2_Constants .NS_MD )
47+ svc_name .set ('{%s}lang' % OneLogin_Saml2_Constants .XML , 'en' )
48+ svc_name .text = acs ['serviceName' ]
49+ try :
50+ svc_description = etree .SubElement (acs_node , "{%s}ServiceDescription" % OneLogin_Saml2_Constants .NS_MD )
51+ svc_description .set ('{%s}lang' % OneLogin_Saml2_Constants .XML , 'en' )
52+ svc_description .text = acs ['serviceDescription' ]
53+ except KeyError :
54+ # serviceDescription is optional
55+ pass
56+
57+ # iterate through all the requested attributes of each service
58+ for req_attribs in acs ['requestedAttributes' ]:
59+
60+ requested_attribute = etree .SubElement (acs_node , "{%s}RequestedAttribute" % OneLogin_Saml2_Constants .NS_MD )
61+ # construct the permissible attrib values, if present
62+ try :
63+ for attrib_val in req_attribs ['attributeValue' ]:
64+ val = etree .SubElement (requested_attribute , "{%s}AttributeValue" % OneLogin_Saml2_Constants .NS_SAML )
65+ val .text = attrib_val
66+ except KeyError :
67+ # it's fine, attributeValue is an optional element
68+ pass
69+
70+ requested_attribute .set ('Name' , req_attribs ['name' ])
71+ try :
72+ requested_attribute .set ('NameFormat' , req_attribs ['nameFormat' ])
73+ except KeyError :
74+ pass
75+
76+ try :
77+ requested_attribute .set ('FriendlyName' , req_attribs ['friendlyName' ])
78+ except KeyError :
79+ pass
80+
81+ try :
82+ requested_attribute .set ('isRequired' , str (req_attribs ['isRequired' ]).lower ())
83+ except KeyError :
84+ pass
85+
86+ attrib_index += 1
87+
3088 @staticmethod
3189 def builder (sp , authnsign = False , wsign = False , valid_until = None , cache_duration = None , contacts = None , organization = None ):
3290 """
@@ -50,7 +108,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
50108 :param contacts: Contacts info
51109 :type contacts: dict
52110
53- :param organization: Organization ingo
111+ :param organization: Organization info
54112 :type organization: dict
55113 """
56114 if valid_until is None :
@@ -76,6 +134,11 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
76134 if organization is None :
77135 organization = {}
78136
137+ try :
138+ attr_consuming_service = sp ['attributeConsumingService' ]
139+ except KeyError :
140+ attr_consuming_service = []
141+
79142 sls = ''
80143 if 'singleLogoutService' in sp and 'url' in sp ['singleLogoutService' ]:
81144 sls = """ <md:SingleLogoutService Binding="%(binding)s"
@@ -119,7 +182,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
119182 str_contacts = '\n ' .join (contacts_info )
120183
121184 metadata = """<?xml version="1.0"?>
122- <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
185+ <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" %(saml_namespace)s
123186 %(valid)s
124187 %(cache)s
125188 entityID="%(entity_id)s">
@@ -144,8 +207,16 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
144207 'sls' : sls ,
145208 'organization' : str_organization ,
146209 'contacts' : str_contacts ,
210+ 'saml_namespace' : 'xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"' if attr_consuming_service else ''
147211 }
148- return metadata
212+
213+ # i'm not sure why the above xml was build by hand. Building via lxml is way easier,
214+ # especially for conditional attributes etc..
215+ # So as a work around, i'm creating a xml dom, insert the attibute_consumer_service
216+ # nodes into it and then return the serialized xml
217+ root = etree .fromstring (metadata )
218+ OneLogin_Saml2_Metadata .add_attribute_consuming_service (root , attr_consuming_service )
219+ return etree .tostring (root , pretty_print = True )
149220
150221 @staticmethod
151222 def sign_metadata (metadata , key , cert , sign_algorithm = OneLogin_Saml2_Constants .RSA_SHA1 ):
0 commit comments