summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorivan <ivan@grnet.gr>2017-07-18 15:14:28 +0300
committerivan <ivan@grnet.gr>2017-10-08 19:47:24 +0300
commit20c961045cb1e2b7b56b2c7eb72595897d1a1477 (patch)
tree7735d7895ba7c2926ea41fbfc263ebaf00c1b1d1
parent144248f968603c1483c56fcbbddab0edfd61613f (diff)
downloadpysaml2-20c961045cb1e2b7b56b2c7eb72595897d1a1477.tar.gz
Add eIDAS RequestedAttributes node support
-rw-r--r--src/saml2/client_base.py56
-rw-r--r--src/saml2/config.py1
-rw-r--r--src/saml2/extension/requested_attributes.py131
-rw-r--r--tests/server_conf.py13
-rw-r--r--tests/test_51_client.py16
-rw-r--r--tools/data/requested_attributes.xsd28
6 files changed, 245 insertions, 0 deletions
diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py
index 88a9bd11..531ddea5 100644
--- a/src/saml2/client_base.py
+++ b/src/saml2/client_base.py
@@ -10,6 +10,8 @@ import six
from saml2.entity import Entity
+import saml2.attributemaps as attributemaps
+
from saml2.mdstore import destinations
from saml2.profile import paos, ecp
from saml2.saml import NAMEID_FORMAT_TRANSIENT
@@ -20,6 +22,7 @@ from saml2.samlp import AuthzDecisionQuery
from saml2.samlp import AuthnRequest
from saml2.samlp import Extensions
from saml2.extension import sp_type
+from saml2.extension import requested_attributes
import saml2
import time
@@ -357,6 +360,59 @@ class Base(Entity):
item = sp_type.SPType(text=conf_sp_type)
extensions.add_extension_element(item)
+ requested_attrs = self.config.getattr('requested_attributes', 'sp')
+ if requested_attrs:
+ if not extensions:
+ extensions = Extensions()
+
+ attributemapsmods = []
+ for modname in attributemaps.__all__:
+ attributemapsmods.append(getattr(attributemaps, modname))
+
+ items = []
+ for attr in requested_attrs:
+ friendly_name = attr.get('friendly_name')
+ name = attr.get('name')
+ name_format = attr.get('name_format')
+ is_required = str(attr.get('required', False)).lower()
+
+ if not name and not friendly_name:
+ raise ValueError(
+ "Missing required attribute: '{}' or '{}'".format(
+ 'name', 'friendly_name'))
+
+ if not name:
+ for mod in attributemapsmods:
+ try:
+ name = mod.MAP['to'][friendly_name]
+ except KeyError:
+ continue
+ else:
+ if not name_format:
+ name_format = mod.MAP['identifier']
+ break
+
+ if not friendly_name:
+ for mod in attributemapsmods:
+ try:
+ friendly_name = mod.MAP['fro'][name]
+ except KeyError:
+ continue
+ else:
+ if not name_format:
+ name_format = mod.MAP['identifier']
+ break
+
+ items.append(requested_attributes.RequestedAttribute(
+ is_required=is_required,
+ name_format=name_format,
+ friendly_name=friendly_name,
+ name=name))
+
+ item = requested_attributes.RequestedAttributes(
+ extension_elements=items)
+ extensions.add_extension_element(item)
+
if kwargs:
_args, extensions = self._filter_args(AuthnRequest(), extensions,
**kwargs)
diff --git a/src/saml2/config.py b/src/saml2/config.py
index 3c8618f4..296f0e85 100644
--- a/src/saml2/config.py
+++ b/src/saml2/config.py
@@ -80,6 +80,7 @@ SP_ARGS = [
"force_authn",
"sp_type",
"sp_type_in_metadata",
+ "requested_attributes",
]
AA_IDP_ARGS = [
diff --git a/src/saml2/extension/requested_attributes.py b/src/saml2/extension/requested_attributes.py
new file mode 100644
index 00000000..3d574f15
--- /dev/null
+++ b/src/saml2/extension/requested_attributes.py
@@ -0,0 +1,131 @@
+#!/usr/bin/env python
+
+#
+# Generated Tue Jul 18 14:58:29 2017 by parse_xsd.py version 0.5.
+#
+
+import saml2
+from saml2 import SamlBase
+
+from saml2 import saml
+
+
+NAMESPACE = 'http://eidas.europa.eu/saml-extensions'
+
+class RequestedAttributeType_(SamlBase):
+ """The http://eidas.europa.eu/saml-extensions:RequestedAttributeType element """
+
+ c_tag = 'RequestedAttributeType'
+ c_namespace = NAMESPACE
+ c_children = SamlBase.c_children.copy()
+ c_attributes = SamlBase.c_attributes.copy()
+ c_child_order = SamlBase.c_child_order[:]
+ c_cardinality = SamlBase.c_cardinality.copy()
+ c_children['{urn:oasis:names:tc:SAML:2.0:assertion}AttributeValue'] = ('attribute_value', [saml.AttributeValue])
+ c_cardinality['attribute_value'] = {"min":0}
+ c_attributes['Name'] = ('name', 'None', True)
+ c_attributes['NameFormat'] = ('name_format', 'None', True)
+ c_attributes['FriendlyName'] = ('friendly_name', 'None', False)
+ c_attributes['isRequired'] = ('is_required', 'None', False)
+ c_child_order.extend(['attribute_value'])
+
+ def __init__(self,
+ attribute_value=None,
+ name=None,
+ name_format=None,
+ friendly_name=None,
+ is_required=None,
+ text=None,
+ extension_elements=None,
+ extension_attributes=None,
+ ):
+ SamlBase.__init__(self,
+ text=text,
+ extension_elements=extension_elements,
+ extension_attributes=extension_attributes,
+ )
+ self.attribute_value=attribute_value or []
+ self.name=name
+ self.name_format=name_format
+ self.friendly_name=friendly_name
+ self.is_required=is_required
+
+def requested_attribute_type__from_string(xml_string):
+ return saml2.create_class_from_xml_string(RequestedAttributeType_, xml_string)
+
+
+class RequestedAttribute(RequestedAttributeType_):
+ """The http://eidas.europa.eu/saml-extensions:RequestedAttribute element """
+
+ c_tag = 'RequestedAttribute'
+ c_namespace = NAMESPACE
+ c_children = RequestedAttributeType_.c_children.copy()
+ c_attributes = RequestedAttributeType_.c_attributes.copy()
+ c_child_order = RequestedAttributeType_.c_child_order[:]
+ c_cardinality = RequestedAttributeType_.c_cardinality.copy()
+
+def requested_attribute_from_string(xml_string):
+ return saml2.create_class_from_xml_string(RequestedAttribute, xml_string)
+
+
+class RequestedAttributesType_(SamlBase):
+ """The http://eidas.europa.eu/saml-extensions:RequestedAttributesType element """
+
+ c_tag = 'RequestedAttributesType'
+ c_namespace = NAMESPACE
+ c_children = SamlBase.c_children.copy()
+ c_attributes = SamlBase.c_attributes.copy()
+ c_child_order = SamlBase.c_child_order[:]
+ c_cardinality = SamlBase.c_cardinality.copy()
+ c_children['{http://eidas.europa.eu/saml-extensions}RequestedAttribute'] = ('requested_attribute', [RequestedAttribute])
+ c_cardinality['requested_attribute'] = {"min":0}
+ c_child_order.extend(['requested_attribute'])
+
+ def __init__(self,
+ requested_attribute=None,
+ text=None,
+ extension_elements=None,
+ extension_attributes=None,
+ ):
+ SamlBase.__init__(self,
+ text=text,
+ extension_elements=extension_elements,
+ extension_attributes=extension_attributes,
+ )
+ self.requested_attribute=requested_attribute or []
+
+def requested_attributes_type__from_string(xml_string):
+ return saml2.create_class_from_xml_string(RequestedAttributesType_, xml_string)
+
+
+class RequestedAttributes(RequestedAttributesType_):
+ """The http://eidas.europa.eu/saml-extensions:RequestedAttributes element """
+
+ c_tag = 'RequestedAttributes'
+ c_namespace = NAMESPACE
+ c_children = RequestedAttributesType_.c_children.copy()
+ c_attributes = RequestedAttributesType_.c_attributes.copy()
+ c_child_order = RequestedAttributesType_.c_child_order[:]
+ c_cardinality = RequestedAttributesType_.c_cardinality.copy()
+
+def requested_attributes_from_string(xml_string):
+ return saml2.create_class_from_xml_string(RequestedAttributes, xml_string)
+
+
+ELEMENT_FROM_STRING = {
+ RequestedAttributes.c_tag: requested_attributes_from_string,
+ RequestedAttributesType_.c_tag: requested_attributes_type__from_string,
+ RequestedAttribute.c_tag: requested_attribute_from_string,
+ RequestedAttributeType_.c_tag: requested_attribute_type__from_string,
+}
+
+ELEMENT_BY_TAG = {
+ 'RequestedAttributes': RequestedAttributes,
+ 'RequestedAttributesType': RequestedAttributesType_,
+ 'RequestedAttribute': RequestedAttribute,
+ 'RequestedAttributeType': RequestedAttributeType_,
+}
+
+
+def factory(tag, **kwargs):
+ return ELEMENT_BY_TAG[tag](**kwargs)
diff --git a/tests/server_conf.py b/tests/server_conf.py
index aa34d8f7..4b528119 100644
--- a/tests/server_conf.py
+++ b/tests/server_conf.py
@@ -14,6 +14,19 @@ CONFIG = {
"required_attributes": ["surName", "givenName", "mail"],
"optional_attributes": ["title"],
"idp": ["urn:mace:example.com:saml:roland:idp"],
+ "requested_attributes": [
+ {
+ "name": "http://eidas.europa.eu/attributes/naturalperson/DateOfBirth",
+ "required": False,
+ },
+ {
+ "friendly_name": "PersonIdentifier",
+ "required": True,
+ },
+ {
+ "friendly_name": "PlaceOfBirth",
+ },
+ ],
}
},
"debug": 1,
diff --git a/tests/test_51_client.py b/tests/test_51_client.py
index bcc535af..2bd4d7cf 100644
--- a/tests/test_51_client.py
+++ b/tests/test_51_client.py
@@ -22,6 +22,8 @@ from saml2 import samlp
from saml2 import sigver
from saml2 import s_utils
from saml2.assertion import Assertion
+from saml2.extension.requested_attributes import RequestedAttributes
+from saml2.extension.requested_attributes import RequestedAttribute
from saml2.authn_context import INTERNETPROTOCOLPASSWORD
from saml2.client import Saml2Client
@@ -280,6 +282,20 @@ class TestClient:
assert nid_policy.allow_create == "false"
assert nid_policy.format == saml.NAMEID_FORMAT_TRANSIENT
+ node_requested_attributes = None
+ for e in ar.extensions.extension_elements:
+ if e.tag == RequestedAttributes.c_tag:
+ node_requested_attributes = e
+ break
+ assert node_requested_attributes is not None
+
+ for c in node_requested_attributes.children:
+ assert c.tag == RequestedAttribute.c_tag
+ assert c.attributes['isRequired'] in ['true', 'false']
+ assert c.attributes['Name']
+ assert c.attributes['FriendlyName']
+ assert c.attributes['NameFormat']
+
def test_create_auth_request_unset_force_authn(self):
req_id, req = self.client.create_authn_request(
"http://www.example.com/sso", sign=False, message_id="id1")
diff --git a/tools/data/requested_attributes.xsd b/tools/data/requested_attributes.xsd
new file mode 100644
index 00000000..b796f3d3
--- /dev/null
+++ b/tools/data/requested_attributes.xsd
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsd:schema
+ xmlns="http://eidas.europa.eu/saml-extensions"
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
+ xmlns:eidas="http://eidas.europa.eu/saml-extensions"
+ targetNamespace="http://eidas.europa.eu/saml-extensions"
+ elementFormDefault="qualified"
+ attributeFormDefault="unqualified"
+ version="1">
+ <xsd:element name="RequestedAttributes" type="eidas:RequestedAttributesType"/>
+ <xsd:complexType name="RequestedAttributesType">
+ <xsd:sequence>
+ <xsd:element minOccurs="0" maxOccurs="unbounded" ref="eidas:RequestedAttribute"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="RequestedAttribute" type="eidas:RequestedAttributeType"/>
+ <xsd:complexType name="RequestedAttributeType">
+ <xsd:sequence>
+ <xsd:element minOccurs="0" maxOccurs="unbounded" ref="saml2:AttributeValue" type="anyType"/>
+ </xsd:sequence>
+ <xsd:attribute name="Name" type="string" use="required"/>
+ <xsd:attribute name="NameFormat" type="anyURI" use="required"/>
+ <xsd:attribute name="FriendlyName" type="string" use="optional"/>
+ <xsd:anyAttribute namespace="##other" processContents="lax"/>
+ <xsd:attribute name="isRequired" type="boolean" use="optional"/>
+ </xsd:complexType>
+</xsd:schema>