Skip to content

Commit 20c9610

Browse files
author
ivan
committed
Add eIDAS RequestedAttributes node support
1 parent 144248f commit 20c9610

File tree

6 files changed

+245
-0
lines changed

6 files changed

+245
-0
lines changed

src/saml2/client_base.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
from saml2.entity import Entity
1212

13+
import saml2.attributemaps as attributemaps
14+
1315
from saml2.mdstore import destinations
1416
from saml2.profile import paos, ecp
1517
from saml2.saml import NAMEID_FORMAT_TRANSIENT
@@ -20,6 +22,7 @@
2022
from saml2.samlp import AuthnRequest
2123
from saml2.samlp import Extensions
2224
from saml2.extension import sp_type
25+
from saml2.extension import requested_attributes
2326

2427
import saml2
2528
import time
@@ -357,6 +360,59 @@ def create_authn_request(self, destination, vorg="", scoping=None,
357360
item = sp_type.SPType(text=conf_sp_type)
358361
extensions.add_extension_element(item)
359362

363+
requested_attrs = self.config.getattr('requested_attributes', 'sp')
364+
if requested_attrs:
365+
if not extensions:
366+
extensions = Extensions()
367+
368+
attributemapsmods = []
369+
for modname in attributemaps.__all__:
370+
attributemapsmods.append(getattr(attributemaps, modname))
371+
372+
items = []
373+
for attr in requested_attrs:
374+
friendly_name = attr.get('friendly_name')
375+
name = attr.get('name')
376+
name_format = attr.get('name_format')
377+
is_required = str(attr.get('required', False)).lower()
378+
379+
if not name and not friendly_name:
380+
raise ValueError(
381+
"Missing required attribute: '{}' or '{}'".format(
382+
'name', 'friendly_name'))
383+
384+
if not name:
385+
for mod in attributemapsmods:
386+
try:
387+
name = mod.MAP['to'][friendly_name]
388+
except KeyError:
389+
continue
390+
else:
391+
if not name_format:
392+
name_format = mod.MAP['identifier']
393+
break
394+
395+
if not friendly_name:
396+
for mod in attributemapsmods:
397+
try:
398+
friendly_name = mod.MAP['fro'][name]
399+
except KeyError:
400+
continue
401+
else:
402+
if not name_format:
403+
name_format = mod.MAP['identifier']
404+
break
405+
406+
items.append(requested_attributes.RequestedAttribute(
407+
is_required=is_required,
408+
name_format=name_format,
409+
friendly_name=friendly_name,
410+
name=name))
411+
412+
item = requested_attributes.RequestedAttributes(
413+
extension_elements=items)
414+
extensions.add_extension_element(item)
415+
360416
if kwargs:
361417
_args, extensions = self._filter_args(AuthnRequest(), extensions,
362418
**kwargs)

src/saml2/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"force_authn",
8181
"sp_type",
8282
"sp_type_in_metadata",
83+
"requested_attributes",
8384
]
8485

8586
AA_IDP_ARGS = [
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env python
2+
3+
#
4+
# Generated Tue Jul 18 14:58:29 2017 by parse_xsd.py version 0.5.
5+
#
6+
7+
import saml2
8+
from saml2 import SamlBase
9+
10+
from saml2 import saml
11+
12+
13+
NAMESPACE = 'http://eidas.europa.eu/saml-extensions'
14+
15+
class RequestedAttributeType_(SamlBase):
16+
"""The http://eidas.europa.eu/saml-extensions:RequestedAttributeType element """
17+
18+
c_tag = 'RequestedAttributeType'
19+
c_namespace = NAMESPACE
20+
c_children = SamlBase.c_children.copy()
21+
c_attributes = SamlBase.c_attributes.copy()
22+
c_child_order = SamlBase.c_child_order[:]
23+
c_cardinality = SamlBase.c_cardinality.copy()
24+
c_children['{urn:oasis:names:tc:SAML:2.0:assertion}AttributeValue'] = ('attribute_value', [saml.AttributeValue])
25+
c_cardinality['attribute_value'] = {"min":0}
26+
c_attributes['Name'] = ('name', 'None', True)
27+
c_attributes['NameFormat'] = ('name_format', 'None', True)
28+
c_attributes['FriendlyName'] = ('friendly_name', 'None', False)
29+
c_attributes['isRequired'] = ('is_required', 'None', False)
30+
c_child_order.extend(['attribute_value'])
31+
32+
def __init__(self,
33+
attribute_value=None,
34+
name=None,
35+
name_format=None,
36+
friendly_name=None,
37+
is_required=None,
38+
text=None,
39+
extension_elements=None,
40+
extension_attributes=None,
41+
):
42+
SamlBase.__init__(self,
43+
text=text,
44+
extension_elements=extension_elements,
45+
extension_attributes=extension_attributes,
46+
)
47+
self.attribute_value=attribute_value or []
48+
self.name=name
49+
self.name_format=name_format
50+
self.friendly_name=friendly_name
51+
self.is_required=is_required
52+
53+
def requested_attribute_type__from_string(xml_string):
54+
return saml2.create_class_from_xml_string(RequestedAttributeType_, xml_string)
55+
56+
57+
class RequestedAttribute(RequestedAttributeType_):
58+
"""The http://eidas.europa.eu/saml-extensions:RequestedAttribute element """
59+
60+
c_tag = 'RequestedAttribute'
61+
c_namespace = NAMESPACE
62+
c_children = RequestedAttributeType_.c_children.copy()
63+
c_attributes = RequestedAttributeType_.c_attributes.copy()
64+
c_child_order = RequestedAttributeType_.c_child_order[:]
65+
c_cardinality = RequestedAttributeType_.c_cardinality.copy()
66+
67+
def requested_attribute_from_string(xml_string):
68+
return saml2.create_class_from_xml_string(RequestedAttribute, xml_string)
69+
70+
71+
class RequestedAttributesType_(SamlBase):
72+
"""The http://eidas.europa.eu/saml-extensions:RequestedAttributesType element """
73+
74+
c_tag = 'RequestedAttributesType'
75+
c_namespace = NAMESPACE
76+
c_children = SamlBase.c_children.copy()
77+
c_attributes = SamlBase.c_attributes.copy()
78+
c_child_order = SamlBase.c_child_order[:]
79+
c_cardinality = SamlBase.c_cardinality.copy()
80+
c_children['{http://eidas.europa.eu/saml-extensions}RequestedAttribute'] = ('requested_attribute', [RequestedAttribute])
81+
c_cardinality['requested_attribute'] = {"min":0}
82+
c_child_order.extend(['requested_attribute'])
83+
84+
def __init__(self,
85+
requested_attribute=None,
86+
text=None,
87+
extension_elements=None,
88+
extension_attributes=None,
89+
):
90+
SamlBase.__init__(self,
91+
text=text,
92+
extension_elements=extension_elements,
93+
extension_attributes=extension_attributes,
94+
)
95+
self.requested_attribute=requested_attribute or []
96+
97+
def requested_attributes_type__from_string(xml_string):
98+
return saml2.create_class_from_xml_string(RequestedAttributesType_, xml_string)
99+
100+
101+
class RequestedAttributes(RequestedAttributesType_):
102+
"""The http://eidas.europa.eu/saml-extensions:RequestedAttributes element """
103+
104+
c_tag = 'RequestedAttributes'
105+
c_namespace = NAMESPACE
106+
c_children = RequestedAttributesType_.c_children.copy()
107+
c_attributes = RequestedAttributesType_.c_attributes.copy()
108+
c_child_order = RequestedAttributesType_.c_child_order[:]
109+
c_cardinality = RequestedAttributesType_.c_cardinality.copy()
110+
111+
def requested_attributes_from_string(xml_string):
112+
return saml2.create_class_from_xml_string(RequestedAttributes, xml_string)
113+
114+
115+
ELEMENT_FROM_STRING = {
116+
RequestedAttributes.c_tag: requested_attributes_from_string,
117+
RequestedAttributesType_.c_tag: requested_attributes_type__from_string,
118+
RequestedAttribute.c_tag: requested_attribute_from_string,
119+
RequestedAttributeType_.c_tag: requested_attribute_type__from_string,
120+
}
121+
122+
ELEMENT_BY_TAG = {
123+
'RequestedAttributes': RequestedAttributes,
124+
'RequestedAttributesType': RequestedAttributesType_,
125+
'RequestedAttribute': RequestedAttribute,
126+
'RequestedAttributeType': RequestedAttributeType_,
127+
}
128+
129+
130+
def factory(tag, **kwargs):
131+
return ELEMENT_BY_TAG[tag](**kwargs)

tests/server_conf.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,19 @@
1414
"required_attributes": ["surName", "givenName", "mail"],
1515
"optional_attributes": ["title"],
1616
"idp": ["urn:mace:example.com:saml:roland:idp"],
17+
"requested_attributes": [
18+
{
19+
"name": "http://eidas.europa.eu/attributes/naturalperson/DateOfBirth",
20+
"required": False,
21+
},
22+
{
23+
"friendly_name": "PersonIdentifier",
24+
"required": True,
25+
},
26+
{
27+
"friendly_name": "PlaceOfBirth",
28+
},
29+
],
1730
}
1831
},
1932
"debug": 1,

tests/test_51_client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from saml2 import sigver
2323
from saml2 import s_utils
2424
from saml2.assertion import Assertion
25+
from saml2.extension.requested_attributes import RequestedAttributes
26+
from saml2.extension.requested_attributes import RequestedAttribute
2527

2628
from saml2.authn_context import INTERNETPROTOCOLPASSWORD
2729
from saml2.client import Saml2Client
@@ -280,6 +282,20 @@ def test_create_auth_request_0(self):
280282
assert nid_policy.allow_create == "false"
281283
assert nid_policy.format == saml.NAMEID_FORMAT_TRANSIENT
282284

285+
node_requested_attributes = None
286+
for e in ar.extensions.extension_elements:
287+
if e.tag == RequestedAttributes.c_tag:
288+
node_requested_attributes = e
289+
break
290+
assert node_requested_attributes is not None
291+
292+
for c in node_requested_attributes.children:
293+
assert c.tag == RequestedAttribute.c_tag
294+
assert c.attributes['isRequired'] in ['true', 'false']
295+
assert c.attributes['Name']
296+
assert c.attributes['FriendlyName']
297+
assert c.attributes['NameFormat']
298+
283299
def test_create_auth_request_unset_force_authn(self):
284300
req_id, req = self.client.create_authn_request(
285301
"http://www.example.com/sso", sign=False, message_id="id1")

tools/data/requested_attributes.xsd

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<xsd:schema
3+
xmlns="http://eidas.europa.eu/saml-extensions"
4+
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
5+
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
6+
xmlns:eidas="http://eidas.europa.eu/saml-extensions"
7+
targetNamespace="http://eidas.europa.eu/saml-extensions"
8+
elementFormDefault="qualified"
9+
attributeFormDefault="unqualified"
10+
version="1">
11+
<xsd:element name="RequestedAttributes" type="eidas:RequestedAttributesType"/>
12+
<xsd:complexType name="RequestedAttributesType">
13+
<xsd:sequence>
14+
<xsd:element minOccurs="0" maxOccurs="unbounded" ref="eidas:RequestedAttribute"/>
15+
</xsd:sequence>
16+
</xsd:complexType>
17+
<xsd:element name="RequestedAttribute" type="eidas:RequestedAttributeType"/>
18+
<xsd:complexType name="RequestedAttributeType">
19+
<xsd:sequence>
20+
<xsd:element minOccurs="0" maxOccurs="unbounded" ref="saml2:AttributeValue" type="anyType"/>
21+
</xsd:sequence>
22+
<xsd:attribute name="Name" type="string" use="required"/>
23+
<xsd:attribute name="NameFormat" type="anyURI" use="required"/>
24+
<xsd:attribute name="FriendlyName" type="string" use="optional"/>
25+
<xsd:anyAttribute namespace="##other" processContents="lax"/>
26+
<xsd:attribute name="isRequired" type="boolean" use="optional"/>
27+
</xsd:complexType>
28+
</xsd:schema>

0 commit comments

Comments
 (0)