Skip to content

Commit 54404b0

Browse files
committed
Add get_friendlyname_attributes support
1 parent 7c01457 commit 54404b0

File tree

5 files changed

+95
-4
lines changed

5 files changed

+95
-4
lines changed

src/onelogin/saml2/auth.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None):
5151
self.__request_data = request_data
5252
self.__settings = OneLogin_Saml2_Settings(old_settings, custom_base_path)
5353
self.__attributes = []
54+
self.__friendlyname_attributes = []
5455
self.__nameid = None
5556
self.__nameid_format = None
5657
self.__nameid_nq = None
@@ -104,6 +105,7 @@ def process_response(self, request_id=None):
104105
self.__last_response = response.get_xml_document()
105106
if response.is_valid(self.__request_data, request_id):
106107
self.__attributes = response.get_attributes()
108+
self.__friendlyname_attributes = response.get_friendlyname_attributes()
107109
self.__nameid = response.get_nameid()
108110
self.__nameid_format = response.get_nameid_format()
109111
self.__nameid_nq = response.get_nameid_nq()
@@ -115,11 +117,9 @@ def process_response(self, request_id=None):
115117
self.__last_authn_contexts = response.get_authn_contexts()
116118
self.__last_assertion_not_on_or_after = response.get_assertion_not_on_or_after()
117119
self.__authenticated = True
118-
119120
else:
120121
self.__errors.append('invalid_response')
121122
self.__error_reason = response.get_error()
122-
123123
else:
124124
self.__errors.append('invalid_binding')
125125
raise OneLogin_Saml2_Error(
@@ -231,6 +231,15 @@ def get_attributes(self):
231231
"""
232232
return self.__attributes
233233

234+
def get_friendlyname_attributes(self):
235+
"""
236+
Returns the set of SAML attributes indexed by FiendlyName.
237+
238+
:returns: SAML attributes
239+
:rtype: dict
240+
"""
241+
return self.__friendlyname_attributes
242+
234243
def get_nameid(self):
235244
"""
236245
Returns the nameID.
@@ -324,6 +333,22 @@ def get_attribute(self, name):
324333
value = self.__attributes[name]
325334
return value
326335

336+
def get_friendlyname_attribute(self, friendlyname):
337+
"""
338+
Returns the requested SAML attribute searched by FriendlyName.
339+
340+
:param friendlyname: FriendlyName of the attribute
341+
:type friendlyname: string
342+
343+
:returns: Attribute value(s) if exists or None
344+
:rtype: list
345+
"""
346+
assert isinstance(friendlyname, basestring)
347+
value = None
348+
if self.__friendlyname_attributes and friendlyname in self.__friendlyname_attributes.keys():
349+
value = self.__friendlyname_attributes[friendlyname]
350+
return value
351+
327352
def get_last_request_id(self):
328353
"""
329354
:returns: The ID of the last Request SAML message generated.

src/onelogin/saml2/response.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,45 @@ def get_attributes(self):
620620
attributes[attr_name] = values
621621
return attributes
622622

623+
def get_friendlyname_attributes(self):
624+
"""
625+
Gets the Attributes from the AttributeStatement element indexed by FiendlyName.
626+
EncryptedAttributes are not supported
627+
"""
628+
attributes = {}
629+
attribute_nodes = self.__query_assertion('/saml:AttributeStatement/saml:Attribute')
630+
for attribute_node in attribute_nodes:
631+
attr_friendlyname = attribute_node.get('FriendlyName')
632+
if attr_friendlyname:
633+
if attr_friendlyname in attributes.keys():
634+
raise OneLogin_Saml2_ValidationError(
635+
'Found an Attribute element with duplicated FriendlyName',
636+
OneLogin_Saml2_ValidationError.DUPLICATED_ATTRIBUTE_NAME_FOUND
637+
)
638+
639+
values = []
640+
for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]):
641+
# Remove any whitespace (which may be present where attributes are
642+
# nested inside NameID children).
643+
attr_text = OneLogin_Saml2_Utils.element_text(attr)
644+
if attr_text:
645+
attr_text = attr_text.strip()
646+
if attr_text:
647+
values.append(attr_text)
648+
649+
# Parse any nested NameID children
650+
for nameid in attr.iterchildren('{%s}NameID' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]):
651+
values.append({
652+
'NameID': {
653+
'Format': nameid.get('Format'),
654+
'NameQualifier': nameid.get('NameQualifier'),
655+
'value': OneLogin_Saml2_Utils.element_text(nameid)
656+
}
657+
})
658+
659+
attributes[attr_friendlyname] = values
660+
return attributes
661+
623662
def validate_num_assertions(self):
624663
"""
625664
Verifies that the document only contains a single Assertion (encrypted or not)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIElEPSJHT1NBTUxSMTI5MDExNzQ1NzE3OTQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiIgRGVzdGluYXRpb249IntyZWNpcGllbnR9Ij4NCiAgPHNhbWxwOlN0YXR1cz4NCiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+DQogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIFZlcnNpb249IjIuMCIgSUQ9InBmeDhmZmIzOTgzLWNiZjYtOTJhMS1mMmM0LTYxOWFlMWJlMWM4NiIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiI+DQogICAgPHNhbWw6SXNzdWVyPmh0dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sL21ldGFkYXRhLzEzNTkwPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4NCiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+DQogIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4OGZmYjM5ODMtY2JmNi05MmExLWYyYzQtNjE5YWUxYmUxYzg2Ij48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT5oZ3VRYkNIYW5pYmJEQzdxM1p6eHpIY1Bvbkk9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPkdhbmNEOXZSb2g5TWJUMDAyRHk3OXQxbTZJNllmaFVLUGZibGttcDJ1ZG9sWHVqdjZlMU1XdnNWbXhOenRzSUdseEFhMHFLRGlTTXpDTkRac2szanN5c1VsMW5BS25BZzE4NWpmWGpzemhzZG1SK005MWR4azZrZmNMVW9zT29sb3ZhZFdMUFdxbjdQM2o4LzV4enA5THBSQTNndkI0MTgyUlNpcldDQlhQUT08L2RzOlNpZ25hdHVyZVZhbHVlPg0KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQ2dUQ0NBZW9DQ1FDYk9scldEZFg3RlRBTkJna3Foa2lHOXcwQkFRVUZBRENCaERFTE1Ba0dBMVVFQmhNQ1RrOHhHREFXQmdOVkJBZ1REMEZ1WkhKbFlYTWdVMjlzWW1WeVp6RU1NQW9HQTFVRUJ4TURSbTl2TVJBd0RnWURWUVFLRXdkVlRrbE9SVlJVTVJnd0ZnWURWUVFERXc5bVpXbGtaUzVsY214aGJtY3VibTh4SVRBZkJna3Foa2lHOXcwQkNRRVdFbUZ1WkhKbFlYTkFkVzVwYm1WMGRDNXViekFlRncwd056QTJNVFV4TWpBeE16VmFGdzB3TnpBNE1UUXhNakF4TXpWYU1JR0VNUXN3Q1FZRFZRUUdFd0pPVHpFWU1CWUdBMVVFQ0JNUFFXNWtjbVZoY3lCVGIyeGlaWEpuTVF3d0NnWURWUVFIRXdOR2IyOHhFREFPQmdOVkJBb1RCMVZPU1U1RlZGUXhHREFXQmdOVkJBTVREMlpsYVdSbExtVnliR0Z1Wnk1dWJ6RWhNQjhHQ1NxR1NJYjNEUUVKQVJZU1lXNWtjbVZoYzBCMWJtbHVaWFIwTG01dk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRRGl2YmhSN1A1MTZ4L1MzQnFLeHVwUWUwTE9Ob2xpdXBpQk9lc0NPM1NIYkRybDMrcTlJYmZuZm1FMDRyTnVNY1BzSXhCMTYxVGREcEllc0xDbjdjOGFQSElTS090UGxBZVRaU25iOFFBdTdhUmpacTMrUGJyUDV1VzNUY2ZDR1B0S1R5dEhPZ2UvT2xKYm8wNzhkVmhYUTE0ZDFFRHdYSlcxclJYdVV0NEM4UUlEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFDRFZmcDg2SE9icVkrZThCVW9XUTkrVk1ReDFBU0RvaEJqd09zZzJXeWtVcVJYRitkTGZjVUg5ZFdSNjNDdFpJS0ZEYlN0Tm9tUG5RejduYksrb255Z3dCc3BWRWJuSHVVaWhacTNaVWRtdW1RcUN3NFV2cy8xVXZxM29yT28vV0pWaFR5dkxnRlZLMlFhclE0LzY3T1pmSGQ3UitQT0JYaG9waFNNdjFaT288L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT4NCiAgICA8c2FtbDpTdWJqZWN0Pg0KICAgICAgPHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj5zdXBwb3J0QG9uZWxvZ2luLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiIFJlY2lwaWVudD0ie3JlY2lwaWVudH0iLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0xMS0xOFQyMTo1MjozN1oiIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+e2F1ZGllbmNlfTwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMC0xMS0xOFQyMTo1NzozN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTAtMTEtMTlUMjE6NTc6MzdaIiBTZXNzaW9uSW5kZXg9Il81MzFjMzJkMjgzYmRmZjdlMDRlNDg3YmNkYmM0ZGQ4ZCI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgRnJpZW5kbHlOYW1lPSJ1c2VybmFtZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+ZGVtbzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iYW5vdGhlcl92YWx1ZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+dmFsdWU8L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogIDwvc2FtbDpBc3NlcnRpb24+DQo8L3NhbWxwOlJlc3BvbnNlPg==

tests/src/OneLogin/saml2_tests/auth_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,8 @@ def testProcessResponseValid(self):
233233
attributes = auth.get_attributes()
234234
self.assertNotEqual(len(attributes), 0)
235235
self.assertEqual(auth.get_attribute('mail'), attributes['mail'])
236+
friendlyname_attributes = auth.get_friendlyname_attributes()
237+
self.assertEqual(len(friendlyname_attributes), 0)
236238
session_index = auth.get_session_index()
237239
self.assertEqual('_6273d77b8cde0c333ec79d22a9fa0003b9fe2d75cb', session_index)
238240
self.assertEqual("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", auth.get_nameid_format())

tests/src/OneLogin/saml2_tests/response_test.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,7 @@ def testGetSessionIndex(self):
648648

649649
def testGetAttributes(self):
650650
"""
651-
Tests the getAttributes method of the OneLogin_Saml2_Response
651+
Tests the get_attributes method of the OneLogin_Saml2_Response
652652
"""
653653
settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
654654
xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
@@ -670,9 +670,33 @@ def testGetAttributes(self):
670670
response_3 = OneLogin_Saml2_Response(settings, xml_3)
671671
self.assertEqual({}, response_3.get_attributes())
672672

673+
def testGetFriendlyAttributes(self):
674+
"""
675+
Tests the get_friendlyname_attributes method of the OneLogin_Saml2_Response
676+
"""
677+
settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
678+
xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
679+
response = OneLogin_Saml2_Response(settings, xml)
680+
self.assertEqual({}, response.get_friendlyname_attributes())
681+
682+
expected_attributes = {
683+
'username': ['demo']
684+
}
685+
xml_2 = self.file_contents(join(self.data_path, 'responses', 'response1_with_friendlyname.xml.base64'))
686+
response_2 = OneLogin_Saml2_Response(settings, xml_2)
687+
self.assertEqual(expected_attributes, response_2.get_friendlyname_attributes())
688+
689+
xml_3 = self.file_contents(join(self.data_path, 'responses', 'response2.xml.base64'))
690+
response_3 = OneLogin_Saml2_Response(settings, xml_3)
691+
self.assertEqual({}, response_3.get_friendlyname_attributes())
692+
693+
xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_attrs.xml.base64'))
694+
response_4 = OneLogin_Saml2_Response(settings, xml_4)
695+
self.assertEqual({}, response_4.get_friendlyname_attributes())
696+
673697
def testGetNestedNameIDAttributes(self):
674698
"""
675-
Tests the getAttributes method of the OneLogin_Saml2_Response with nested
699+
Tests the get_attributes method of the OneLogin_Saml2_Response with nested
676700
nameID data
677701
"""
678702
settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())

0 commit comments

Comments
 (0)