diff options
author | Roland Hedberg <roland@catalogix.se> | 2017-10-11 08:32:10 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-10-11 08:32:10 +0200 |
commit | b71f1443991c94099ff49226c92b8671d657fade (patch) | |
tree | 69cb8fb94681a3273d1fe995e0e79bb047d307cf | |
parent | 546f9d4ca82c8b4ae5cf7aa0fb80f0bedf385c89 (diff) | |
parent | 9e6ddc0030828a17fe44a3ecdc5c95b6b6c8d741 (diff) | |
download | pysaml2-b71f1443991c94099ff49226c92b8671d657fade.tar.gz |
Merge branch 'master' into master
38 files changed, 830 insertions, 180 deletions
@@ -38,3 +38,15 @@ testing. To run the tests on your system's version of python To run tests in multiple python environments, you can use `pyenv <https://github.com/yyuu/pyenv>`_ with `tox <https://tox.readthedocs.io/en/latest/>`_. + + +Please contribute! +================== + +To help out, you could: + +1. Test and report any bugs or other difficulties. +2. Implement missing features. +3. Write more unit tests. + +**If you have the time and inclination I'm looking for Collaborators** diff --git a/src/saml2/algsupport.py b/src/saml2/algsupport.py index f9bc06b8..72036b40 100644 --- a/src/saml2/algsupport.py +++ b/src/saml2/algsupport.py @@ -23,7 +23,7 @@ SIGNING_METHODS = { "rsa-sha256": 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', "rsa-sha384": 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384', "rsa-sha512": 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512', - "dsa-sha1": 'http,//www.w3.org/2000/09/xmldsig#dsa-sha1', + "dsa-sha1": 'http://www.w3.org/2000/09/xmldsig#dsa-sha1', 'dsa-sha256': 'http://www.w3.org/2009/xmldsig11#dsa-sha256', 'ecdsa_sha1': 'http://www.w3.org/2001/04/xmldsig-more#ECDSA_sha1', 'ecdsa_sha224': 'http://www.w3.org/2001/04/xmldsig-more#ECDSA_sha224', diff --git a/src/saml2/assertion.py b/src/saml2/assertion.py index 64944d11..0db4b723 100644 --- a/src/saml2/assertion.py +++ b/src/saml2/assertion.py @@ -78,19 +78,22 @@ def filter_on_attributes(ava, required=None, optional=None, acs=None, """ def _match_attr_name(attr, ava): - try: - friendly_name = attr["friendly_name"] - except KeyError: - friendly_name = get_local_name(acs, attr["name"], - attr["name_format"]) + + local_name = get_local_name(acs, attr["name"], attr["name_format"]) + if not local_name: + try: + local_name = attr["friendly_name"] + except KeyError: + pass - _fn = _match(friendly_name, ava) + _fn = _match(local_name, ava) if not _fn: # In the unlikely case that someone has provided us with # URIs as attribute names _fn = _match(attr["name"], ava) return _fn + def _apply_attr_value_restrictions(attr, res, must=False): try: values = [av["text"] for av in attr["attribute_value"]] @@ -105,7 +108,6 @@ def filter_on_attributes(ava, required=None, optional=None, acs=None, return _filter_values(ava[_fn], values, must) res = {} - if required is None: required = [] diff --git a/src/saml2/attribute_converter.py b/src/saml2/attribute_converter.py index 3d52a816..3d32d226 100644 --- a/src/saml2/attribute_converter.py +++ b/src/saml2/attribute_converter.py @@ -246,7 +246,7 @@ def get_local_name(acs, attr, name_format): for aconv in acs: #print(ac.format, name_format) if aconv.name_format == name_format: - return aconv._fro[attr] + return aconv._fro.get(attr) def d_to_local_name(acs, attr): diff --git a/src/saml2/attributemaps/saml_uri.py b/src/saml2/attributemaps/saml_uri.py index ca6dfd84..e97090ff 100644 --- a/src/saml2/attributemaps/saml_uri.py +++ b/src/saml2/attributemaps/saml_uri.py @@ -13,10 +13,19 @@ SCHAC = 'urn:oid:1.3.6.1.4.1.25178.1.2.' SIS = 'urn:oid:1.2.752.194.10.2.' UMICH = 'urn:oid:1.3.6.1.4.1.250.1.57.' OPENOSI_OID = 'urn:oid:1.3.6.1.4.1.27630.2.1.1.' #openosi-0.82.schema http://www.openosi.org/osi/display/ldap/Home +EIDAS_NATURALPERSON = 'http://eidas.europa.eu/attributes/naturalperson/' MAP = { 'identifier': 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri', 'fro': { + EIDAS_NATURALPERSON+'PersonIdentifier': 'PersonIdentifier', + EIDAS_NATURALPERSON+'FamilyName': 'FamilyName', + EIDAS_NATURALPERSON+'FirstName': 'FirstName', + EIDAS_NATURALPERSON+'DateOfBirth': 'DateOfBirth', + EIDAS_NATURALPERSON+'BirthName': 'BirthName', + EIDAS_NATURALPERSON+'PlaceOfBirth': 'PlaceOfBirth', + EIDAS_NATURALPERSON+'CurrentAddress': 'CurrentAddress', + EIDAS_NATURALPERSON+'Gender': 'Gender', EDUCOURSE_OID+'1': 'eduCourseOffering', EDUCOURSE_OID+'2': 'eduCourseMember', EDUMEMBER1_OID+'1': 'isMemberOf', @@ -161,6 +170,14 @@ MAP = { X500ATTR_OID+'65': 'pseudonym', }, 'to': { + 'PersonIdentifier': EIDAS_NATURALPERSON+'PersonIdentifier', + 'FamilyName': EIDAS_NATURALPERSON+'FamilyName', + 'FirstName': EIDAS_NATURALPERSON+'FirstName', + 'DateOfBirth': EIDAS_NATURALPERSON+'DateOfBirth', + 'BirthName': EIDAS_NATURALPERSON+'BirthName', + 'PlaceOfBirth': EIDAS_NATURALPERSON+'PlaceOfBirth', + 'CurrentAddress': EIDAS_NATURALPERSON+'CurrentAddress', + 'Gender': EIDAS_NATURALPERSON+'Gender', 'associatedDomain': UCL_DIR_PILOT+'37', 'authorityRevocationList': X500ATTR_OID+'38', 'businessCategory': X500ATTR_OID+'15', diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 55e5b1fc..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 @@ -18,6 +20,9 @@ from saml2.samlp import NameIDMappingRequest from saml2.samlp import AttributeQuery 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 @@ -207,7 +212,7 @@ class Base(Entity): nameid_format=None, service_url_binding=None, message_id=0, consent=None, extensions=None, sign=None, - allow_create=False, sign_prepare=False, sign_alg=None, + allow_create=None, sign_prepare=False, sign_alg=None, digest_alg=None, **kwargs): """ Creates an authentication request. @@ -235,26 +240,30 @@ class Base(Entity): args = {} - try: - args["assertion_consumer_service_url"] = kwargs[ - "assertion_consumer_service_urls"][0] - del kwargs["assertion_consumer_service_urls"] - except KeyError: + if self.config.getattr('hide_assertion_consumer_service', 'sp'): + args["assertion_consumer_service_url"] = None + binding = None + else: try: args["assertion_consumer_service_url"] = kwargs[ - "assertion_consumer_service_url"] - del kwargs["assertion_consumer_service_url"] + "assertion_consumer_service_urls"][0] + del kwargs["assertion_consumer_service_urls"] except KeyError: try: - args["assertion_consumer_service_index"] = str(kwargs[ - "assertion_consumer_service_index"]) - del kwargs["assertion_consumer_service_index"] + args["assertion_consumer_service_url"] = kwargs[ + "assertion_consumer_service_url"] + del kwargs["assertion_consumer_service_url"] except KeyError: - if service_url_binding is None: - service_urls = self.service_urls(binding) - else: - service_urls = self.service_urls(service_url_binding) - args["assertion_consumer_service_url"] = service_urls[0] + try: + args["assertion_consumer_service_index"] = str( + kwargs["assertion_consumer_service_index"]) + del kwargs["assertion_consumer_service_index"] + except KeyError: + if service_url_binding is None: + service_urls = self.service_urls(binding) + else: + service_urls = self.service_urls(service_url_binding) + args["assertion_consumer_service_url"] = service_urls[0] try: args["provider_name"] = kwargs["provider_name"] @@ -268,7 +277,7 @@ class Base(Entity): # all of these have cardinality 0..1 _msg = AuthnRequest() for param in ["scoping", "requested_authn_context", "conditions", - "subject", "scoping"]: + "subject"]: try: _item = kwargs[param] except KeyError: @@ -288,10 +297,15 @@ class Base(Entity): args["name_id_policy"] = kwargs["name_id_policy"] del kwargs["name_id_policy"] except KeyError: - if allow_create: - allow_create = "true" - else: - allow_create = "false" + if allow_create is None: + allow_create = self.config.getattr("name_id_format_allow_create", "sp") + if allow_create is None: + allow_create = "false" + else: + if allow_create is True: + allow_create = "true" + else: + allow_create = "false" if nameid_format == "": name_id_policy = None @@ -299,12 +313,21 @@ class Base(Entity): if nameid_format is None: nameid_format = self.config.getattr("name_id_format", "sp") + # If no nameid_format has been set in the configuration + # or passed in then transient is the default. if nameid_format is None: nameid_format = NAMEID_FORMAT_TRANSIENT + + # If a list has been configured or passed in choose the + # first since NameIDPolicy can only have one format specified. elif isinstance(nameid_format, list): - # NameIDPolicy can only have one format specified nameid_format = nameid_format[0] + # Allow a deployer to signal that no format should be specified + # in the NameIDPolicy by passing in or configuring the string 'None'. + elif nameid_format == 'None': + nameid_format = None + name_id_policy = samlp.NameIDPolicy(allow_create=allow_create, format=nameid_format) @@ -321,6 +344,75 @@ class Base(Entity): except KeyError: nsprefix = None + try: + force_authn = kwargs['force_authn'] + except KeyError: + force_authn = self.config.getattr('force_authn', 'sp') + finally: + if force_authn: + args['force_authn'] = 'true' + + conf_sp_type = self.config.getattr('sp_type', 'sp') + conf_sp_type_in_md = self.config.getattr('sp_type_in_metadata', 'sp') + if conf_sp_type and conf_sp_type_in_md is False: + if not extensions: + extensions = Extensions() + 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 9fc3e708..296f0e85 100644 --- a/src/saml2/config.py +++ b/src/saml2/config.py @@ -73,8 +73,14 @@ SP_ARGS = [ "allow_unsolicited", "ecp", "name_id_format", + "name_id_format_allow_create", "logout_requests_signed", - "requested_attribute_name_format" + "requested_attribute_name_format", + "hide_assertion_consumer_service", + "force_authn", + "sp_type", + "sp_type_in_metadata", + "requested_attributes", ] AA_IDP_ARGS = [ @@ -187,6 +193,7 @@ class Config(object): self.contact_person = None self.name_form = None self.name_id_format = None + self.name_id_format_allow_create = None self.virtual_organization = None self.logger = None self.only_use_keys_in_metadata = True @@ -205,7 +212,6 @@ class Config(object): self.crypto_backend = 'xmlsec1' self.scope = "" self.allow_unknown_attributes = False - self.allow_unsolicited = False self.extension_schema = {} self.cert_handler_extra_class = None self.verify_encrypt_cert_advice = None diff --git a/src/saml2/ecp.py b/src/saml2/ecp.py index f15a259c..5817cda4 100644 --- a/src/saml2/ecp.py +++ b/src/saml2/ecp.py @@ -24,6 +24,8 @@ from saml2.schema import soapenv from saml2.response import authn_response +from saml2 import saml + logger = logging.getLogger(__name__) @@ -53,7 +55,7 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False): # ---------------------------------------- # <paos:Request> # ---------------------------------------- - my_url = cls.service_url(BINDING_PAOS) + my_url = cls.service_urls(BINDING_PAOS)[0] # must_understand and actor according to the standard # @@ -64,6 +66,19 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False): eelist.append(element_to_extension_element(paos_request)) # ---------------------------------------- + # <samlp:AuthnRequest> + # ---------------------------------------- + + logger.info("entityid: %s, binding: %s" % (entityid, BINDING_SOAP)) + + location = cls._sso_location(entityid, binding=BINDING_SOAP) + req_id, authn_req = cls.create_authn_request( + location, binding=BINDING_PAOS, service_url_binding=BINDING_PAOS) + + body = soapenv.Body() + body.extension_elements = [element_to_extension_element(authn_req)] + + # ---------------------------------------- # <ecp:Request> # ---------------------------------------- @@ -74,14 +89,16 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False): # ) # # idp_list = samlp.IDPList(idp_entry= [idp]) -# -# ecp_request = ecp.Request( -# actor = ACTOR, must_understand = "1", -# provider_name = "Example Service Provider", -# issuer=saml.Issuer(text="https://sp.example.org/entity"), -# idp_list = idp_list) -# -# eelist.append(element_to_extension_element(ecp_request)) + + idp_list = None + ecp_request = ecp.Request( + actor=ACTOR, + must_understand="1", + provider_name=None, + issuer=saml.Issuer(text=authn_req.issuer.text), + idp_list=idp_list) + + eelist.append(element_to_extension_element(ecp_request)) # ---------------------------------------- # <ecp:RelayState> @@ -96,19 +113,6 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False): header.extension_elements = eelist # ---------------------------------------- - # <samlp:AuthnRequest> - # ---------------------------------------- - - logger.info("entityid: %s, binding: %s" % (entityid, BINDING_SOAP)) - - location = cls._sso_location(entityid, binding=BINDING_SOAP) - req_id, authn_req = cls.create_authn_request( - location, binding=BINDING_PAOS, service_url_binding=BINDING_PAOS) - - body = soapenv.Body() - body.extension_elements = [element_to_extension_element(authn_req)] - - # ---------------------------------------- # The SOAP envelope # ---------------------------------------- @@ -126,7 +130,7 @@ def handle_ecp_authn_response(cls, soap_message, outstanding=None): if item.c_tag == "RelayState" and item.c_namespace == ecp.NAMESPACE: _relay_state = item - response = authn_response(cls.config, cls.service_url(), outstanding, + response = authn_response(cls.config, cls.service_urls(), outstanding, allow_unsolicited=True) response.loads("%s" % rdict["body"], False, soap_message) diff --git a/src/saml2/ecp_client.py b/src/saml2/ecp_client.py index 90c3b44c..788d252d 100644 --- a/src/saml2/ecp_client.py +++ b/src/saml2/ecp_client.py @@ -119,7 +119,7 @@ class Client(Entity): if response.status_code != 200: raise SAMLError( "Request to IdP failed (%s): %s" % (response.status_code, - response.error)) + response.text)) # SAMLP response in a SOAP envelope body, ecp response in headers respdict = self.parse_soap_message(response.text) @@ -200,22 +200,19 @@ class Client(Entity): ht_args = self.use_soap(idp_response, args["rc_url"], [args["relay_state"]]) - + ht_args["headers"][0] = ('Content-Type', MIME_PAOS) logger.debug("[P3] Post to SP: %s", ht_args["data"]) - ht_args["headers"].append(('Content-Type', 'application/vnd.paos+xml')) - # POST the package from the IdP to the SP - response = self.send(args["rc_url"], "POST", **ht_args) + response = self.send(**ht_args) if response.status_code == 302: # ignore where the SP is redirecting us to and go for the # url I started off with. pass else: - print(response.error) raise SAMLError( - "Error POSTing package to SP: %s" % response.error) + "Error POSTing package to SP: %s" % response.text) logger.debug("[P3] SP response: %s", response.text) @@ -255,8 +252,7 @@ class Client(Entity): :param opargs: Arguments to the HTTP call :return: The page """ - if url not in opargs: - url = self._sp + sp_url = self._sp # ******************************************** # Phase 1 - First conversation with the SP @@ -264,13 +260,13 @@ class Client(Entity): # headers needed to indicate to the SP that I'm ECP enabled opargs["headers"] = self.add_paos_headers(opargs["headers"]) - - response = self.send(url, op, **opargs) - logger.debug("[Op] SP response: %s", response) + response = self.send(sp_url, op, **opargs) + logger.debug("[Op] SP response: %s" % response) + print(response.text) if response.status_code != 200: raise SAMLError( - "Request to SP failed: %s" % response.error) + "Request to SP failed: %s" % response.text) # The response might be a AuthnRequest instance in a SOAP envelope # body. If so it's the start of the ECP conversation @@ -282,7 +278,6 @@ class Client(Entity): # header blocks may also be present try: respdict = self.parse_soap_message(response.text) - self.ecp_conversation(respdict, idp_entity_id) # should by now be authenticated so this should go smoothly @@ -290,11 +285,9 @@ class Client(Entity): except (soap.XmlParseError, AssertionError, KeyError): pass - #print("RESP",response, self.http.response) - - if response.status_code != 404: + if response.status_code >= 400: raise SAMLError("Error performing operation: %s" % ( - response.error,)) + response.text,)) return response diff --git a/src/saml2/entity.py b/src/saml2/entity.py index b24c6210..27b30fe9 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -8,7 +8,7 @@ from binascii import hexlify from hashlib import sha1 from saml2.metadata import ENDPOINTS -from saml2.profile import paos, ecp +from saml2.profile import paos, ecp, samlec from saml2.soap import parse_soap_enveloped_saml_artifact_resolve from saml2.soap import class_instances_from_soap_enveloped_saml_thingies from saml2.soap import open_soap_envelope @@ -224,7 +224,7 @@ class Entity(HTTPBase): info["method"] = "POST" elif binding == BINDING_HTTP_REDIRECT: logger.info("HTTP REDIRECT") - if 'sigalg' in kwargs: + if kwargs.get('sigalg', ''): signer = self.sec.sec_backend.get_signer(kwargs['sigalg']) else: signer = None @@ -407,7 +407,8 @@ class Entity(HTTPBase): """ return class_instances_from_soap_enveloped_saml_thingies(text, [paos, ecp, - samlp]) + samlp, + samlec]) @staticmethod def unpack_soap_message(text): 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/src/saml2/extension/sp_type.py b/src/saml2/extension/sp_type.py new file mode 100644 index 00000000..8ffb2cea --- /dev/null +++ b/src/saml2/extension/sp_type.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +# +# Generated Tue Jul 18 15:03:44 2017 by parse_xsd.py version 0.5. +# + +import saml2 +from saml2 import SamlBase + + +NAMESPACE = 'http://eidas.europa.eu/saml-extensions' + +class SPTypeType_(SamlBase): + """The http://eidas.europa.eu/saml-extensions:SPTypeType element """ + + c_tag = 'SPTypeType' + c_namespace = NAMESPACE + c_value_type = {'base': 'xsd:string', 'enumeration': ['public', 'private']} + 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() + +def sp_type_type__from_string(xml_string): + return saml2.create_class_from_xml_string(SPTypeType_, xml_string) + + +class SPType(SPTypeType_): + """The http://eidas.europa.eu/saml-extensions:SPType element """ + + c_tag = 'SPType' + c_namespace = NAMESPACE + c_children = SPTypeType_.c_children.copy() + c_attributes = SPTypeType_.c_attributes.copy() + c_child_order = SPTypeType_.c_child_order[:] + c_cardinality = SPTypeType_.c_cardinality.copy() + +def sp_type_from_string(xml_string): + return saml2.create_class_from_xml_string(SPType, xml_string) + + +ELEMENT_FROM_STRING = { + SPType.c_tag: sp_type_from_string, + SPTypeType_.c_tag: sp_type_type__from_string, +} + +ELEMENT_BY_TAG = { + 'SPType': SPType, + 'SPTypeType': SPTypeType_, +} + + +def factory(tag, **kwargs): + return ELEMENT_BY_TAG[tag](**kwargs) diff --git a/src/saml2/mdstore.py b/src/saml2/mdstore.py index eff75c8b..72825ea8 100644 --- a/src/saml2/mdstore.py +++ b/src/saml2/mdstore.py @@ -750,7 +750,7 @@ class MetaDataExtern(InMemoryMetaData): """ response = self.http.send(self.url) if response.status_code == 200: - _txt = response.text.encode("utf-8") + _txt = response.content return self.parse_and_check_signature(_txt) else: logger.info("Response status: %s", response.status_code) @@ -814,7 +814,7 @@ class MetaDataMDX(InMemoryMetaData): response = requests.get(mdx_url, headers={ 'Accept': SAML_METADATA_CONTENT_TYPE}) if response.status_code == 200: - _txt = response.text.encode("utf-8") + _txt = response.content if self.parse_and_check_signature(_txt): return self.entity[item] diff --git a/src/saml2/metadata.py b/src/saml2/metadata.py index 50ec0bae..de2e6e75 100644 --- a/src/saml2/metadata.py +++ b/src/saml2/metadata.py @@ -9,6 +9,7 @@ from saml2.extension import mdui from saml2.extension import idpdisc from saml2.extension import shibmd from saml2.extension import mdattr +from saml2.extension import sp_type from saml2.saml import NAME_FORMAT_URI from saml2.saml import AttributeValue from saml2.saml import Attribute @@ -722,7 +723,8 @@ def entity_descriptor(confd): entd.contact_person = do_contact_person_info(confd.contact_person) if confd.entity_category: - entd.extensions = md.Extensions() + if not entd.extensions: + entd.extensions = md.Extensions() ava = [AttributeValue(text=c) for c in confd.entity_category] attr = Attribute(attribute_value=ava, name="http://macedir.org/entity-category") @@ -734,6 +736,14 @@ def entity_descriptor(confd): entd.extensions = md.Extensions() entd.extensions.add_extension_element(item) + conf_sp_type = confd.getattr('sp_type', 'sp') + conf_sp_type_in_md = confd.getattr('sp_type_in_metadata', 'sp') + if conf_sp_type and conf_sp_type_in_md is True: + if not entd.extensions: + entd.extensions = md.Extensions() + item = sp_type.SPType(text=conf_sp_type) + entd.extensions.add_extension_element(item) + serves = confd.serves if not serves: raise SAMLError( diff --git a/src/saml2/pack.py b/src/saml2/pack.py index 728a516f..3bf39fc8 100644 --- a/src/saml2/pack.py +++ b/src/saml2/pack.py @@ -40,12 +40,35 @@ except ImportError: import defusedxml.ElementTree NAMESPACE = "http://schemas.xmlsoap.org/soap/envelope/" -FORM_SPEC = """<form method="post" action="%s"> - <input type="hidden" name="%s" value="%s" /> - <input type="hidden" name="RelayState" value="%s" /> - <input type="submit" value="Submit" /> -</form>""" +FORM_SPEC = """\ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + </head> + <body onload="document.forms[0].submit()"> + <noscript> + <p> + <strong>Note:</strong> Since your browser does not support JavaScript, + you must press the Continue button once to proceed. + </p> + </noscript> + + <form action="{action}" method="post"> + <div> + <input type="hidden" name="RelayState" value="{relay_state}"/> + + <input type="hidden" name="{saml_type}" value="{saml_response}"/> + </div> + <noscript> + <div> + <input type="submit" value="Continue"/> + </div> + </noscript> + </form> + </body> +</html>""" def http_form_post_message(message, location, relay_state="", typ="SAMLRequest", **kwargs): @@ -58,8 +81,6 @@ def http_form_post_message(message, location, relay_state="", :param relay_state: for preserving and conveying state information :return: A tuple containing header information and a HTML message. """ - response = ["<head>", """<title>SAML 2.0 POST</title>""", "</head><body>"] - if not isinstance(message, six.string_types): message = str(message) if not isinstance(message, six.binary_type): @@ -71,17 +92,17 @@ def http_form_post_message(message, location, relay_state="", _msg = message _msg = _msg.decode('ascii') - response.append(FORM_SPEC % (location, typ, _msg, relay_state)) + args = { + 'action' : location, + 'saml_type' : typ, + 'relay_state' : relay_state, + 'saml_response' : _msg + } - response.append("""<script type="text/javascript">""") - response.append(" window.onload = function ()") - response.append(" { document.forms[0].submit(); }") - response.append("""</script>""") - response.append("</body>") + response = FORM_SPEC.format(**args) return {"headers": [("Content-type", "text/html")], "data": response} - def http_post_message(message, relay_state="", typ="SAMLRequest", **kwargs): """ diff --git a/src/saml2/profile/samlec.py b/src/saml2/profile/samlec.py new file mode 100644 index 00000000..b90f6d3d --- /dev/null +++ b/src/saml2/profile/samlec.py @@ -0,0 +1,14 @@ +from saml2 import SamlBase + + +NAMESPACE = 'urn:ietf:params:xml:ns:samlec' + + +class GeneratedKey(SamlBase): + c_tag = 'GeneratedKey' + c_namespace = NAMESPACE + + +ELEMENT_BY_TAG = { + 'GeneratedKey': GeneratedKey, +} diff --git a/src/saml2/response.py b/src/saml2/response.py index 13323509..6de8723b 100644 --- a/src/saml2/response.py +++ b/src/saml2/response.py @@ -666,7 +666,7 @@ class AuthnResponse(StatusResponse): _attr_statem = _assertion.attribute_statement[0] ava.update(self.read_attribute_statement(_attr_statem)) if not ava: - logger.error("Missing Attribute Statement") + logger.debug("Assertion contains no attribute statements") return ava def _bearer_confirmed(self, data): @@ -910,7 +910,8 @@ class AuthnResponse(StatusResponse): else: # This is a saml2int limitation try: assert len(self.response.assertion) == 1 or \ - len(self.response.encrypted_assertion) == 1 + len(self.response.encrypted_assertion) == 1 or \ + self.assertion is not None except AssertionError: raise Exception("No assertion part") diff --git a/src/saml2/saml.py b/src/saml2/saml.py index 35b7bd1a..c53aab95 100644 --- a/src/saml2/saml.py +++ b/src/saml2/saml.py @@ -139,10 +139,12 @@ class AttributeValueBase(SamlBase): if self._extatt: self.extension_attributes = self._extatt - if not text: - self.extension_attributes = {XSI_NIL: 'true'} - else: + if text: self.set_text(text) + elif not extension_elements: + self.extension_attributes = {XSI_NIL: 'true'} + elif XSI_TYPE in self.extension_attributes: + del self.extension_attributes[XSI_TYPE] def __setattr__(self, key, value): if key == "text": diff --git a/src/saml2/validate.py b/src/saml2/validate.py index de68fc00..9fe12c4d 100644 --- a/src/saml2/validate.py +++ b/src/saml2/validate.py @@ -3,6 +3,7 @@ from six.moves.urllib.parse import urlparse import re import struct import base64 +import time from saml2 import time_util @@ -42,8 +43,8 @@ NCNAME = re.compile("(?P<NCName>[a-zA-Z_](\w|[_.-])*)") def valid_ncname(name): match = NCNAME.match(name) - if not match: - raise NotValid("NCName") + #if not match: # hack for invalid authnRequest/ID from meteor saml lib + # raise NotValid("NCName") return True @@ -90,8 +91,10 @@ def validate_on_or_after(not_on_or_after, slack): now = time_util.utc_now() nooa = calendar.timegm(time_util.str_to_time(not_on_or_after)) if now > nooa + slack: + now_str=time.strftime('%Y-%M-%dT%H:%M:%SZ', time.gmtime(now)) raise ResponseLifetimeExceed( - "Can't use it, it's too old %d > %d" % (now - slack, nooa)) + "Can't use repsonse, too old (now=%s + slack=%d > " \ + "not_on_or_after=%s" % (now_str, slack, not_on_or_after)) return nooa else: return False @@ -102,8 +105,9 @@ def validate_before(not_before, slack): now = time_util.utc_now() nbefore = calendar.timegm(time_util.str_to_time(not_before)) if nbefore > now + slack: - raise ToEarly("Can't use it yet %d <= %d" % (now + slack, nbefore)) - + now_str = time.strftime('%Y-%M-%dT%H:%M:%SZ', time.gmtime(now)) + raise ToEarly("Can't use response yet: (now=%s + slack=%d) " + "<= notbefore=%s" % (now_str, slack, not_before)) return True diff --git a/src/saml2/xmldsig/__init__.py b/src/saml2/xmldsig/__init__.py index e00f199d..144cdf54 100644 --- a/src/saml2/xmldsig/__init__.py +++ b/src/saml2/xmldsig/__init__.py @@ -29,7 +29,7 @@ DIGEST_ALLOWED_ALG = (('DIGEST_SHA1', DIGEST_SHA1), ('DIGEST_RIPEMD160', DIGEST_RIPEMD160)) DIGEST_AVAIL_ALG = DIGEST_ALLOWED_ALG + (('DIGEST_MD5', DIGEST_MD5),) -SIG_DSA_SHA1 = 'http,//www.w3.org/2000/09/xmldsig#dsa-sha1' +SIG_DSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' SIG_DSA_SHA256 = 'http://www.w3.org/2009/xmldsig11#dsa-sha256' SIG_ECDSA_SHA1 = 'http://www.w3.org/2001/04/xmldsig-more#ECDSA_sha1' SIG_ECDSA_SHA224 = 'http://www.w3.org/2001/04/xmldsig-more#ECDSA_sha224' diff --git a/tests/SWITCHaaiRootCA.crt.pem b/tests/SWITCHaaiRootCA.crt.pem new file mode 100644 index 00000000..66c9e5d0 --- /dev/null +++ b/tests/SWITCHaaiRootCA.crt.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE-----
+MIIDnzCCAoegAwIBAgINSWITCHaai+Root+CAzANBgkqhkiG9w0BAQUFADBrMQsw
+CQYDVQQGEwJDSDFAMD4GA1UEChM3U3dpdGNoIC0gVGVsZWluZm9ybWF0aWtkaWVu
+c3RlIGZ1ZXIgTGVocmUgdW5kIEZvcnNjaHVuZzEaMBgGA1UEAxMRU1dJVENIYWFp
+IFJvb3QgQ0EwHhcNMDgwNTE1MDYzMDAwWhcNMjgwNTE1MDYyOTU5WjBrMQswCQYD
+VQQGEwJDSDFAMD4GA1UEChM3U3dpdGNoIC0gVGVsZWluZm9ybWF0aWtkaWVuc3Rl
+IGZ1ZXIgTGVocmUgdW5kIEZvcnNjaHVuZzEaMBgGA1UEAxMRU1dJVENIYWFpIFJv
+b3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDUSWbn/rhWew/s
+LJRyciyRKDGyFXSgiDO/EohYuZLw6EAKLLlhZorNtEHQbbn0Oo13S33MclHMvGWT
+KJM0u1hG+6gLy78EPmJbqAE1Uv23wVEH4SX0VJfl3JVqIebiAH/CjuLubgMUspDI
+jOdQHNLS7pthTbm7Tgh7zMsiLPyMTZJep5CGbqv8NoK6bMaF0Z+Bt7e1JRlhHFCV
+iJJaR/+hfpzLsJ8NWVivvrpRGaGJ1XR+9FGsTkjNdMCirNJJZ6XvUOe5w7pHSd9M
+cppFP0eyLs02AMzMXI4iz6PK/w3EdzXGXpK+gSgvLxWYct4xHpv1e2NXhNgdJOSN
+9ra/wJLVAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG
+MB0GA1UdDgQWBBTpmuIGWOsP14EDXVyXubG1k307hDANBgkqhkiG9w0BAQUFAAOC
+AQEAMV/eIW6pFB+mbk7rD7hUPTWDRaoca3kHqmFGFnHfuY8+c0/Mqjh8Y/jyX1yb
+f58crTSWrbyGbUZ3oxDGQ34tuZSkmeR32NqryiX3sP5qlNSozVguQKt8o4vhS1Qe
+WPsXALs3em2pdKuIGSOpbuDnopPcmU2g5Zi2R5P7qpKDKAKtNUEwV+LW7GBMEksO
+Nj7BFXk4AFBFBijaYJGgHmoKSImVgeNIvsV+BSv5HJ4q6vcxfnwuvvGHM0AGphYO
+6f5qtHMUgvAblI8M/2QsBgethaGrirtKJ3aCRLdaR2R1QfaGRpck/Ron5/MpMxiJ
+wLT8YlW/zjx2yNABhPSAjfzeMw==
+-----END CERTIFICATE-----
diff --git a/tests/conftest.py b/tests/conftest.py index 3a895627..5048394c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,18 @@ import os +import pytest #TODO: On my system this function seems to be returning an incorrect location -def pytest_funcarg__xmlsec(request): +@pytest.fixture +def xmlsec(request): for path in os.environ["PATH"].split(":"): fil = os.path.join(path, "xmlsec1") if os.access(fil,os.X_OK): return fil raise Exception("Can't find xmlsec1") - -def pytest_funcarg__AVA(request): + +@pytest.fixture +def AVA(request): return [ { "surName": ["Jeter"], @@ -27,4 +30,4 @@ def pytest_funcarg__AVA(request): "surName": ["Hedberg"], "givenName": ["Roland"], }, - ] + ] 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/sp_conf_nameidpolicy.py b/tests/sp_conf_nameidpolicy.py new file mode 100644 index 00000000..d15989c2 --- /dev/null +++ b/tests/sp_conf_nameidpolicy.py @@ -0,0 +1,64 @@ +from pathutils import full_path +from pathutils import xmlsec_path + +CONFIG = { + "entityid": "urn:mace:example.com:saml:roland:sp", + "name": "urn:mace:example.com:saml:roland:sp", + "description": "My own SP", + "service": { + "sp": { + "endpoints": { + "assertion_consumer_service": [ + "http://lingon.catalogix.se:8087/"], + }, + "required_attributes": ["surName", "givenName", "mail"], + "optional_attributes": ["title"], + "idp": ["urn:mace:example.com:saml:roland:idp"], + "name_id_format": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + "name_id_format_allow_create": "true" + } + }, + "debug": 1, + "key_file": full_path("test.key"), + "cert_file": full_path("test.pem"), + "encryption_keypairs": [{"key_file": full_path("test_1.key"), "cert_file": full_path("test_1.crt")}, + {"key_file": full_path("test_2.key"), "cert_file": full_path("test_2.crt")}], + "ca_certs": full_path("cacerts.txt"), + "xmlsec_binary": xmlsec_path, + "metadata": [{ + "class": "saml2.mdstore.MetaDataFile", + "metadata": [(full_path("idp.xml"), ), (full_path("vo_metadata.xml"), )], + }], + "virtual_organization": { + "urn:mace:example.com:it:tek": { + "nameid_format": "urn:oid:1.3.6.1.4.1.1466.115.121.1.15-NameID", + "common_identifier": "umuselin", + } + }, + "subject_data": "subject_data.db", + "accepted_time_diff": 60, + "attribute_map_dir": full_path("attributemaps"), + "valid_for": 6, + "organization": { + "name": ("AB Exempel", "se"), + "display_name": ("AB Exempel", "se"), + "url": "http://www.example.org", + }, + "contact_person": [{ + "given_name": "Roland", + "sur_name": "Hedberg", + "telephone_number": "+46 70 100 0000", + "email_address": ["tech@eample.com", + "tech@example.org"], + "contact_type": "technical" + }, + ], + "logger": { + "rotating": { + "filename": full_path("sp.log"), + "maxBytes": 100000, + "backupCount": 5, + }, + "loglevel": "info", + } +} diff --git a/tests/sp_mdext_conf.py b/tests/sp_mdext_conf.py index 67e33414..b1f0cf42 100644 --- a/tests/sp_mdext_conf.py +++ b/tests/sp_mdext_conf.py @@ -6,6 +6,8 @@ CONFIG = { "description": "My own SP", "service": { "sp": { + "sp_type": "public", + "sp_type_in_metadata": True, "endpoints": { "assertion_consumer_service": [ "http://lingon.catalogix.se:8087/"], diff --git a/tests/test_19_attribute_converter.py b/tests/test_19_attribute_converter.py index 0fa807b7..8662feee 100644 --- a/tests/test_19_attribute_converter.py +++ b/tests/test_19_attribute_converter.py @@ -10,6 +10,7 @@ from saml2.attribute_converter import AttributeConverter from saml2.attribute_converter import to_local from saml2.saml import attribute_from_string, name_id_from_string, NameID, NAMEID_FORMAT_PERSISTENT from saml2.saml import attribute_statement_from_string +import saml2.attributemaps.saml_uri as saml_map def _eq(l1, l2): @@ -139,12 +140,14 @@ class TestAC(): def test_to_local_name_from_unspecified(self): _xml = """<?xml version='1.0' encoding='UTF-8'?> <ns0:AttributeStatement xmlns:ns0="urn:oasis:names:tc:SAML:2.0:assertion"> -<ns0:Attribute - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - Name="EmailAddress" - NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"> - <ns0:AttributeValue xsi:type="xs:string">foo@bar.com</ns0:AttributeValue> -</ns0:Attribute></ns0:AttributeStatement>""" + <ns0:Attribute + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + Name="EmailAddress" + NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"> + <ns0:AttributeValue xsi:type="xs:string">foo@bar.com</ns0:AttributeValue> + </ns0:Attribute> + </ns0:AttributeStatement> + """ attr = attribute_statement_from_string(_xml) ava = attribute_converter.to_local(self.acs, attr) @@ -236,26 +239,70 @@ def test_noop_attribute_conversion(): assert attr.attribute_value[0].text == "Roland" -ava = """<?xml version='1.0' encoding='UTF-8'?> -<ns0:Attribute xmlns:ns0="urn:oasis:names:tc:SAML:2.0:assertion" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - FriendlyName="schacHomeOrganization" Name="urn:oid:1.3.6.1.4.1.25178.1.2.9" - NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> - <ns0:AttributeValue xsi:nil="true" xsi:type="xs:string"> - uu.se - </ns0:AttributeValue> -</ns0:Attribute>""" +class BuilderAVA(): + def __init__(self, name, friendly_name, name_format): + template = """<?xml version='1.0' encoding='UTF-8'?> + <ns0:Attribute xmlns:ns0="urn:oasis:names:tc:SAML:2.0:assertion" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + Name="{attr_name}" + FriendlyName="{attr_friendly_name}" + NameFormat="{attr_name_format}"> + <ns0:AttributeValue xsi:nil="true" xsi:type="xs:string"> + uu.se + </ns0:AttributeValue> + </ns0:Attribute> + """ + + self.ava = template.format( + attr_name=name, + attr_friendly_name=friendly_name, + attr_name_format=name_format) + + +class TestSchac(): + def test(self): + failures = 0 + friendly_name = "schacHomeOrganization" + ava_schac = BuilderAVA( + "urn:oid:1.3.6.1.4.1.25178.1.2.9", + friendly_name, + saml_map.MAP['identifier']) + + attr = attribute_from_string(ava_schac.ava) + acs = attribute_converter.ac_factory() + + for ac in acs: + try: + res = ac.ava_from(attr) + except KeyError: + failures += 1 + else: + assert res[0] == "schacHomeOrganization" + assert failures != len(acs) -def test_schac(): - attr = attribute_from_string(ava) - acs = attribute_converter.ac_factory() - for ac in acs: - try: - res = ac.ava_from(attr) - assert res[0] == "schacHomeOrganization" - except KeyError: - pass + +class TestEIDAS(): + def test(self): + failures = 0 + friendly_name = 'PersonIdentifier' + ava_eidas = BuilderAVA( + saml_map.EIDAS_NATURALPERSON + friendly_name, + friendly_name, + saml_map.MAP['identifier']) + + attr = attribute_from_string(ava_eidas.ava) + acs = attribute_converter.ac_factory() + + for ac in acs: + try: + res = ac.ava_from(attr) + except KeyError: + failures += 1 + else: + assert res[0] == friendly_name + + assert failures != len(acs) if __name__ == "__main__": diff --git a/tests/test_20_assertion.py b/tests/test_20_assertion.py index ae661d53..5fc36f6b 100644 --- a/tests/test_20_assertion.py +++ b/tests/test_20_assertion.py @@ -64,7 +64,7 @@ def test_filter_on_attributes_0(): required = [a] ava = {"serialNumber": ["12345"]} - ava = filter_on_attributes(ava, required) + ava = filter_on_attributes(ava, required, acs=ac_factory()) assert list(ava.keys()) == ["serialNumber"] assert ava["serialNumber"] == ["12345"] @@ -76,11 +76,23 @@ def test_filter_on_attributes_1(): required = [a] ava = {"serialNumber": ["12345"], "givenName": ["Lars"]} - ava = filter_on_attributes(ava, required) + ava = filter_on_attributes(ava, required, acs=ac_factory()) assert list(ava.keys()) == ["serialNumber"] assert ava["serialNumber"] == ["12345"] +def test_filter_on_attributes_2(): + + a = to_dict(Attribute(friendly_name="surName",name="urn:oid:2.5.4.4", + name_format=NAME_FORMAT_URI), ONTS) + required = [a] + ava = {"sn":["kakavas"]} + + ava = filter_on_attributes(ava,required,acs=ac_factory()) + assert list(ava.keys()) == ['sn'] + assert ava["sn"] == ["kakavas"] + + def test_filter_on_attributes_without_friendly_name(): ava = {"eduPersonTargetedID": "test@example.com", "eduPersonAffiliation": "test", @@ -106,7 +118,7 @@ def test_filter_on_attributes_with_missing_required_attribute(): name="urn:oid:1.3.6.1.4.1.5923.1.1.1.10", name_format=NAME_FORMAT_URI), ONTS) with pytest.raises(MissingValue): - filter_on_attributes(ava, required=[eptid]) + filter_on_attributes(ava, required=[eptid], acs=ac_factory()) def test_filter_on_attributes_with_missing_optional_attribute(): @@ -115,7 +127,7 @@ def test_filter_on_attributes_with_missing_optional_attribute(): friendly_name="eduPersonTargetedID", name="urn:oid:1.3.6.1.4.1.5923.1.1.1.10", name_format=NAME_FORMAT_URI), ONTS) - assert filter_on_attributes(ava, optional=[eptid]) == {} + assert filter_on_attributes(ava, optional=[eptid], acs=ac_factory()) == {} # ---------------------------------------------------------------------- @@ -420,7 +432,7 @@ def test_filter_values_req_2(): required = [a1, a2] ava = {"serialNumber": ["12345"], "givenName": ["Lars"]} - raises(MissingValue, filter_on_attributes, ava, required) + raises(MissingValue, filter_on_attributes, ava, required, acs=ac_factory()) def test_filter_values_req_3(): @@ -432,7 +444,7 @@ def test_filter_values_req_3(): required = [a] ava = {"serialNumber": ["12345"]} - ava = filter_on_attributes(ava, required) + ava = filter_on_attributes(ava, required, acs=ac_factory()) assert list(ava.keys()) == ["serialNumber"] assert ava["serialNumber"] == ["12345"] @@ -446,7 +458,7 @@ def test_filter_values_req_4(): required = [a] ava = {"serialNumber": ["12345"]} - raises(MissingValue, filter_on_attributes, ava, required) + raises(MissingValue, filter_on_attributes, ava, required, acs=ac_factory()) def test_filter_values_req_5(): @@ -458,7 +470,7 @@ def test_filter_values_req_5(): required = [a] ava = {"serialNumber": ["12345", "54321"]} - ava = filter_on_attributes(ava, required) + ava = filter_on_attributes(ava, required, acs=ac_factory()) assert list(ava.keys()) == ["serialNumber"] assert ava["serialNumber"] == ["12345"] @@ -472,7 +484,7 @@ def test_filter_values_req_6(): required = [a] ava = {"serialNumber": ["12345", "54321"]} - ava = filter_on_attributes(ava, required) + ava = filter_on_attributes(ava, required, acs=ac_factory()) assert list(ava.keys()) == ["serialNumber"] assert ava["serialNumber"] == ["54321"] @@ -489,7 +501,7 @@ def test_filter_values_req_opt_0(): ava = {"serialNumber": ["12345", "54321"]} - ava = filter_on_attributes(ava, [r], [o]) + ava = filter_on_attributes(ava, [r], [o], acs=ac_factory()) assert list(ava.keys()) == ["serialNumber"] assert _eq(ava["serialNumber"], ["12345", "54321"]) @@ -507,7 +519,7 @@ def test_filter_values_req_opt_1(): ava = {"serialNumber": ["12345", "54321"]} - ava = filter_on_attributes(ava, [r], [o]) + ava = filter_on_attributes(ava, [r], [o], acs=ac_factory()) assert list(ava.keys()) == ["serialNumber"] assert _eq(ava["serialNumber"], ["12345", "54321"]) @@ -543,7 +555,7 @@ def test_filter_values_req_opt_2(): ava = {"surname": ["Hedberg"], "givenName": ["Roland"], "eduPersonAffiliation": ["staff"], "uid": ["rohe0002"]} - raises(MissingValue, "filter_on_attributes(ava, r, o)") + raises(MissingValue, "filter_on_attributes(ava, r, o, acs=ac_factory())") # --------------------------------------------------------------------------- @@ -923,3 +935,4 @@ def test_assertion_with_authn_instant(): if __name__ == "__main__": test_assertion_2() + diff --git a/tests/test_30_mdstore.py b/tests/test_30_mdstore.py index aadd7726..2a79c86a 100644 --- a/tests/test_30_mdstore.py +++ b/tests/test_30_mdstore.py @@ -7,12 +7,13 @@ from collections import OrderedDict from future.backports.urllib.parse import quote_plus from saml2.config import Config -from saml2.mdstore import MetadataStore +from saml2.mdstore import MetadataStore, MetaDataExtern from saml2.mdstore import MetaDataMDX from saml2.mdstore import SAML_METADATA_CONTENT_TYPE from saml2.mdstore import destinations from saml2.mdstore import name from saml2 import sigver +from saml2.httpbase import HTTPBase from saml2 import BINDING_SOAP from saml2 import BINDING_HTTP_REDIRECT from saml2 import BINDING_HTTP_POST @@ -385,6 +386,14 @@ def test_load_local(): assert cfg +def test_load_remote_encoding(): + crypto = sigver._get_xmlsec_cryptobackend() + sc = sigver.SecurityContext(crypto, key_type="", cert_type="") + httpc = HTTPBase() + mds = MetaDataExtern(ATTRCONV, 'http://metadata.aai.switch.ch/metadata.aaitest.xml', sc, full_path('SWITCHaaiRootCA.crt.pem'), httpc) + mds.load() + + def test_load_string(): sec_config.xmlsec_binary = sigver.get_xmlsec_binary(["/opt/local/bin"]) mds = MetadataStore(ATTRCONV, sec_config, diff --git a/tests/test_31_config.py b/tests/test_31_config.py index 623c944f..eb8480c6 100644 --- a/tests/test_31_config.py +++ b/tests/test_31_config.py @@ -68,6 +68,7 @@ sp2 = { }, "authn_requests_signed": True, "logout_requests_signed": True, + "force_authn": True, } }, #"xmlsec_binary" : "/opt/local/bin/xmlsec1", @@ -408,5 +409,15 @@ def test_crypto_backend(): sec = security_context(idpc) assert isinstance(sec.crypto, CryptoBackendXMLSecurity) +def test_unset_force_authn(): + cnf = SPConfig().load(sp1) + assert bool(cnf.getattr('force_authn', 'sp')) == False + + +def test_set_force_authn(): + cnf = SPConfig().load(sp2) + assert bool(cnf.getattr('force_authn', 'sp')) == True + + if __name__ == "__main__": test_crypto_backend() diff --git a/tests/test_50_server.py b/tests/test_50_server.py index 4aa834c5..f0dcae3c 100644 --- a/tests/test_50_server.py +++ b/tests/test_50_server.py @@ -96,7 +96,7 @@ class TestServer1(): self.client = client.Saml2Client(conf) self.name_id = self.server.ident.transient_nameid( "urn:mace:example.com:saml:roland:sp", "id12") - self.ava = {"givenName": ["Derek"], "surName": ["Jeter"], + self.ava = {"givenName": ["Derek"], "sn": ["Jeter"], "mail": ["derek@nyy.mlb.com"], "title": "The man"} def teardown_class(self): @@ -110,7 +110,7 @@ class TestServer1(): assert ava ==\ {'mail': ['derek@nyy.mlb.com'], 'givenName': ['Derek'], - 'surName': ['Jeter'], 'title': ['The man']} + 'sn': ['Jeter'], 'title': ['The man']} def verify_encrypted_assertion(self, assertion, decr_text): @@ -145,7 +145,7 @@ class TestServer1(): format=saml.NAMEID_FORMAT_TRANSIENT)), attribute_statement=do_attribute_statement( { - ("", "", "surName"): ("Jeter", ""), + ("", "", "sn"): ("Jeter", ""), ("", "", "givenName"): ("Derek", ""), } ), @@ -164,12 +164,12 @@ class TestServer1(): attr1 = attribute_statement.attribute[1] if attr0.attribute_value[0].text == "Derek": assert attr0.friendly_name == "givenName" - assert attr1.friendly_name == "surName" + assert attr1.friendly_name == "sn" assert attr1.attribute_value[0].text == "Jeter" else: assert attr1.friendly_name == "givenName" assert attr1.attribute_value[0].text == "Derek" - assert attr0.friendly_name == "surName" + assert attr0.friendly_name == "sn" assert attr0.attribute_value[0].text == "Jeter" # subject = assertion.subject @@ -187,7 +187,7 @@ class TestServer1(): name_id=saml.NAMEID_FORMAT_TRANSIENT), attribute_statement=do_attribute_statement( { - ("", "", "surName"): ("Jeter", ""), + ("", "", "sn"): ("Jeter", ""), ("", "", "givenName"): ("Derek", ""), } ), @@ -277,7 +277,7 @@ class TestServer1(): resp = self.server.create_authn_response( { "eduPersonEntitlement": "Short stop", - "surName": "Jeter", + "sn": "Jeter", "givenName": "Derek", "mail": "derek.jeter@nyy.mlb.com", "title": "The man" @@ -394,7 +394,7 @@ class TestServer1(): conf.load_file("server_conf") self.client = client.Saml2Client(conf) - ava = {"givenName": ["Derek"], "surName": ["Jeter"], + ava = {"givenName": ["Derek"], "sn": ["Jeter"], "mail": ["derek@nyy.mlb.com"], "title": "The man"} npolicy = samlp.NameIDPolicy(format=saml.NAMEID_FORMAT_TRANSIENT, @@ -425,7 +425,7 @@ class TestServer1(): def test_signed_response(self): name_id = self.server.ident.transient_nameid( "urn:mace:example.com:saml:roland:sp", "id12") - ava = {"givenName": ["Derek"], "surName": ["Jeter"], + ava = {"givenName": ["Derek"], "sn": ["Jeter"], "mail": ["derek@nyy.mlb.com"], "title": "The man"} signed_resp = self.server.create_authn_response( @@ -1139,7 +1139,7 @@ class TestServer1(): "not_on_or_after": soon, "user": { "givenName": "Leo", - "surName": "Laport", + "sn": "Laport", } } self.client.users.add_information_about_person(sinfo) @@ -1163,7 +1163,7 @@ class TestServer1(): "not_on_or_after": soon, "user": { "givenName": "Leo", - "surName": "Laport", + "sn": "Laport", } } @@ -1188,7 +1188,7 @@ class TestServer1(): #------------------------------------------------------------------------ IDENTITY = {"eduPersonAffiliation": ["staff", "member"], - "surName": ["Jeter"], "givenName": ["Derek"], + "sn": ["Jeter"], "givenName": ["Derek"], "mail": ["foo@gmail.com"], "title": "The man"} @@ -1234,7 +1234,7 @@ def _logout_request(conf_file): "not_on_or_after": soon, "user": { "givenName": "Leo", - "surName": "Laport", + "sn": "Laport", } } sp.users.add_information_about_person(sinfo) diff --git a/tests/test_51_client.py b/tests/test_51_client.py index 13cef7cc..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,51 @@ 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") + assert bool(req.force_authn) == False + + def test_create_auth_request_set_force_authn(self): + req_id, req = self.client.create_authn_request( + "http://www.example.com/sso", sign=False, message_id="id1", + force_authn="true") + assert bool(req.force_authn) == True + + def test_create_auth_request_nameid_policy_allow_create(self): + conf = config.SPConfig() + conf.load_file("sp_conf_nameidpolicy") + client = Saml2Client(conf) + ar_str = "%s" % client.create_authn_request( + "http://www.example.com/sso", message_id="id1")[1] + + ar = samlp.authn_request_from_string(ar_str) + print(ar) + assert ar.assertion_consumer_service_url == ("http://lingon.catalogix" + ".se:8087/") + assert ar.destination == "http://www.example.com/sso" + assert ar.protocol_binding == BINDING_HTTP_POST + assert ar.version == "2.0" + assert ar.provider_name == "urn:mace:example.com:saml:roland:sp" + assert ar.issuer.text == "urn:mace:example.com:saml:roland:sp" + nid_policy = ar.name_id_policy + assert nid_policy.allow_create == "true" + assert nid_policy.format == saml.NAMEID_FORMAT_PERSISTENT + def test_create_auth_request_vo(self): assert list(self.client.config.vorg.keys()) == [ "urn:mace:example.com:it:tek"] @@ -346,7 +393,7 @@ class TestClient: def test_response_1(self): IDP = "urn:mace:example.com:saml:roland:idp" - ava = {"givenName": ["Derek"], "surName": ["Jeter"], + ava = {"givenName": ["Derek"], "sn": ["Jeter"], "mail": ["derek@nyy.mlb.com"], "title": ["The man"]} nameid_policy = samlp.NameIDPolicy(allow_create="false", @@ -394,7 +441,7 @@ class TestClient: # --- authenticate another person - ava = {"givenName": ["Alfonson"], "surName": ["Soriano"], + ava = {"givenName": ["Alfonson"], "sn": ["Soriano"], "mail": ["alfonson@chc.mlb.com"], "title": ["outfielder"]} resp_str = "%s" % self.server.create_authn_response( @@ -712,7 +759,7 @@ class TestClient: def setup_verify_authn_response(self): idp = "urn:mace:example.com:saml:roland:idp" - ava = {"givenName": ["Derek"], "surName": ["Jeter"], + ava = {"givenName": ["Derek"], "sn": ["Jeter"], "mail": ["derek@nyy.mlb.com"], "title": ["The man"]} ava_verify = {'mail': ['derek@nyy.mlb.com'], 'givenName': ['Derek'], 'sn': ['Jeter'], 'title': ["The man"]} @@ -761,7 +808,7 @@ class TestClient: format=saml.NAMEID_FORMAT_TRANSIENT)), attribute_statement=do_attribute_statement( { - ("", "", "surName"): ("Jeter", ""), + ("", "", "sn"): ("Jeter", ""), ("", "", "givenName"): ("Derek", ""), } ), @@ -825,7 +872,7 @@ class TestClient: nameid_policy = samlp.NameIDPolicy(allow_create="false", format=saml.NAMEID_FORMAT_PERSISTENT) - asser = Assertion({"givenName": "Derek", "surName": "Jeter"}) + asser = Assertion({"givenName": "Derek", "sn": "Jeter"}) farg = add_path( {}, ['assertion', 'subject', 'subject_confirmation', 'method', @@ -896,7 +943,7 @@ class TestClient: nameid_policy = samlp.NameIDPolicy(allow_create="false", format=saml.NAMEID_FORMAT_PERSISTENT) - asser = Assertion({"givenName": "Derek", "surName": "Jeter"}) + asser = Assertion({"givenName": "Derek", "sn": "Jeter"}) subject_confirmation_specs = { 'recipient': "http://lingon.catalogix.se:8087/", @@ -1027,7 +1074,7 @@ class TestClient: name_id=name_id, farg=farg['assertion']) - asser_2 = Assertion({"surName": "Jeter"}) + asser_2 = Assertion({"sn": "Jeter"}) assertion_2 = asser_2.construct( self.client.config.entityid, @@ -1313,7 +1360,7 @@ class TestClient: "not_on_or_after": in_a_while(minutes=15), "ava": { "givenName": "Anders", - "surName": "Andersson", + "sn": "Andersson", "mail": "anders.andersson@example.com" } } @@ -1350,7 +1397,7 @@ class TestClient: "not_on_or_after": in_a_while(minutes=15), "ava": { "givenName": "Anders", - "surName": "Andersson", + "sn": "Andersson", "mail": "anders.andersson@example.com" }, "session_index": SessionIndex("_foo") @@ -1367,7 +1414,7 @@ class TestClient: binding, info = resp[entity_ids[0]] assert binding == BINDING_HTTP_POST - _dic = unpack_form(info["data"][3]) + _dic = unpack_form(info["data"]) res = self.server.parse_logout_request(_dic["SAMLRequest"], BINDING_HTTP_POST) assert b'<ns0:SessionIndex>_foo</ns0:SessionIndex>' in res.xmlstr @@ -1380,7 +1427,7 @@ class TestClient: "not_on_or_after": a_while_ago(minutes=15), "ava": { "givenName": "Anders", - "surName": "Andersson", + "sn": "Andersson", "mail": "anders.andersson@example.com" }, "session_index": SessionIndex("_foo") @@ -1397,7 +1444,7 @@ class TestClient: binding, info = resp[entity_ids[0]] assert binding == BINDING_HTTP_POST - _dic = unpack_form(info["data"][3]) + _dic = unpack_form(info["data"]) res = self.server.parse_logout_request(_dic["SAMLRequest"], BINDING_HTTP_POST) assert b'<ns0:SessionIndex>_foo</ns0:SessionIndex>' in res.xmlstr @@ -1473,7 +1520,7 @@ class TestClientWithDummy(): "not_on_or_after": in_a_while(minutes=15), "ava": { "givenName": "Anders", - "surName": "Andersson", + "sn": "Andersson", "mail": "anders.andersson@example.com" } } @@ -1494,7 +1541,7 @@ class TestClientWithDummy(): sid, http_args = self.client.prepare_for_authenticate( "urn:mace:example.com:saml:roland:idp", relay_state="really", binding=binding, response_binding=response_binding) - _dic = unpack_form(http_args["data"][3]) + _dic = unpack_form(http_args["data"]) req = self.server.parse_authn_request(_dic["SAMLRequest"], binding) resp_args = self.server.response_args(req.message, [response_binding]) @@ -1512,7 +1559,7 @@ class TestClientWithDummy(): response = self.client.send(**http_args) print(response.text) - _dic = unpack_form(response.text[3], "SAMLResponse") + _dic = unpack_form(response.text, "SAMLResponse") resp = self.client.parse_authn_request_response(_dic["SAMLResponse"], BINDING_HTTP_POST, {sid: "/"}) @@ -1527,7 +1574,7 @@ class TestClientWithDummy(): sid, auth_binding, http_args = self.client.prepare_for_negotiated_authenticate( "urn:mace:example.com:saml:roland:idp", relay_state="really", binding=binding, response_binding=response_binding) - _dic = unpack_form(http_args["data"][3]) + _dic = unpack_form(http_args["data"]) assert binding == auth_binding @@ -1547,7 +1594,7 @@ class TestClientWithDummy(): response = self.client.send(**http_args) print(response.text) - _dic = unpack_form(response.text[3], "SAMLResponse") + _dic = unpack_form(response.text, "SAMLResponse") resp = self.client.parse_authn_request_response(_dic["SAMLResponse"], BINDING_HTTP_POST, {sid: "/"}) diff --git a/tests/test_65_authn_query.py b/tests/test_65_authn_query.py index 54d529f8..68af10a1 100644 --- a/tests/test_65_authn_query.py +++ b/tests/test_65_authn_query.py @@ -28,7 +28,7 @@ def get_msg(hinfo, binding): if binding == BINDING_SOAP: xmlstr = hinfo["data"] elif binding == BINDING_HTTP_POST: - _inp = hinfo["data"][3] + _inp = hinfo["data"] i = _inp.find(TAG1) i += len(TAG1) + 1 j = _inp.find('"', i) diff --git a/tests/test_68_assertion_id.py b/tests/test_68_assertion_id.py index 52959f3a..283b4da6 100644 --- a/tests/test_68_assertion_id.py +++ b/tests/test_68_assertion_id.py @@ -27,7 +27,7 @@ def get_msg(hinfo, binding, response=False): if binding == BINDING_SOAP: msg = hinfo["data"] elif binding == BINDING_HTTP_POST: - _inp = hinfo["data"][3] + _inp = hinfo["data"] i = _inp.find(TAG1) i += len(TAG1) + 1 j = _inp.find('"', i) diff --git a/tests/test_83_md_extensions.py b/tests/test_83_md_extensions.py index 71f98868..dace10a5 100644 --- a/tests/test_83_md_extensions.py +++ b/tests/test_83_md_extensions.py @@ -1,5 +1,6 @@ from saml2.config import Config from saml2.metadata import entity_descriptor +from saml2.extension.sp_type import SPType __author__ = 'roland' @@ -14,4 +15,13 @@ assert ed.spsso_descriptor.extensions assert len(ed.spsso_descriptor.extensions.extension_elements) == 3 assert ed.extensions -assert len(ed.extensions.extension_elements) > 1
\ No newline at end of file +assert len(ed.extensions.extension_elements) > 1 + +assert any(e.tag is SPType.c_tag for e in ed.extensions.extension_elements) + +cnf.setattr('sp', 'sp_type_in_metadata', False) +ed = entity_descriptor(cnf) + +print(ed) + +assert all(e.tag is not SPType.c_tag for e in ed.extensions.extension_elements) diff --git a/tests/test_requirements.txt b/tests/test_requirements.txt index 2c1ebbbc..33301079 100644 --- a/tests/test_requirements.txt +++ b/tests/test_requirements.txt @@ -2,3 +2,4 @@ mock==2.0.0 pymongo==3.0.1 pytest==3.0.3 responses==0.5.0 +pyasn1==0.2.3 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> diff --git a/tools/data/sp_type.xsd b/tools/data/sp_type.xsd new file mode 100644 index 00000000..dbb1418d --- /dev/null +++ b/tools/data/sp_type.xsd @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xsd:schema + xmlns="http://eidas.europa.eu/saml-extensions" + xmlns:xsd="http://www.w3.org/2001/XMLSchema" + targetNamespace="http://eidas.europa.eu/saml-extensions" + elementFormDefault="qualified" + attributeFormDefault="unqualified" + version="1"> + <xsd:element name="SPType" type="SPTypeType"/> + <xsd:simpleType name="SPTypeType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="public"/> + <xsd:enumeration value="private"/> + </xsd:restriction> + </xsd:simpleType> +</xsd:schema> @@ -3,4 +3,4 @@ envlist = py27,py34 [testenv] deps = -rtests/test_requirements.txt -commands = py.test tests/ +commands = py.test {posargs:tests/} |