def test_nonblocking_validation_failure(self): xml = '<xml></xml>' first_nonblocking_validator = FailValidator( XMLSchemaValidationError(['a nonblocking error'])) second_nonblocking_validator = FailValidator( SPIDValidationError(['another nonblocking error'])) validators = [ first_nonblocking_validator, second_nonblocking_validator, ] request = FakeRequest(xml) deserializer = HTTPRequestDeserializer( request, validators=validators, saml_class=FakeSAMLClass) with pytest.raises(DeserializationError) as excinfo: deserializer.deserialize() exc = excinfo.value self.assertEqual(len(exc.details), 2) self.assertEqual(exc.details[0], 'a nonblocking error') self.assertEqual(exc.details[1], 'another nonblocking error') self.assertEqual(exc.initial_data, xml)
def validate(self, request): xmlstr = request.saml_request data = saml_to_dict(xmlstr) if self._action == 'login': req_type = 'AuthnRequest' elif self._action == 'logout': req_type = 'LogoutRequest' issuer_name = data.get( '{urn:oasis:names:tc:SAML:2.0:protocol}%s' % (req_type), {}).get('children', {}).get('{urn:oasis:names:tc:SAML:2.0:assertion}Issuer', {}).get('text') if issuer_name is None: raise UnknownEntityIDError( 'Issuer non presente nella {}'.format(req_type)) if issuer_name and issuer_name not in self._registry.service_providers: raise UnknownEntityIDError( 'L\'entity ID "{}" indicato nell\'elemento <Issuer> non corrisponde a nessun Service Provider registrato in questo Identity Provider di test.' .format(issuer_name)) sp_metadata = self._registry.get(issuer_name) if sp_metadata is not None: atcss = sp_metadata.attribute_consuming_services attribute_consuming_service_indexes = [ str(el.get('attrs').get('index')) for el in atcss if 'index' in el.get('attrs', {}) ] ascss = sp_metadata.assertion_consumer_services assertion_consumer_service_indexes = [ str(el.get('index')) for el in ascss ] assertion_consumer_service_urls = [ str(el.get('Location')) for el in ascss ] else: attribute_consuming_service_indexes = [] assertion_consumer_service_indexes = [] assertion_consumer_service_urls = [] entity_id = self._config.entity_id issuer = Schema( { 'attrs': { 'Format': Equal( NAMEID_FORMAT_ENTITY, msg=DEFAULT_VALUE_ERROR.format(NAMEID_FORMAT_ENTITY)), 'NameQualifier': Any(Url(), Match(r'^urn:'), msg="Invalid URI"), }, 'children': {}, 'text': str, }, required=True, ) name_id = Schema( { 'attrs': { 'NameQualifier': str, 'Format': Equal(NAMEID_FORMAT_TRANSIENT, msg=DEFAULT_VALUE_ERROR.format( NAMEID_FORMAT_TRANSIENT)), }, 'children': {}, 'text': str }, required=True, ) name_id_policy = Schema( { 'attrs': { 'Format': Equal(NAMEID_FORMAT_TRANSIENT, msg=DEFAULT_VALUE_ERROR.format( NAMEID_FORMAT_TRANSIENT)), Optional('SPNameQualifier'): str, }, 'children': {}, 'text': None, }, required=True, ) conditions = Schema( { 'attrs': { 'NotBefore': All(str, _check_utc_date), 'NotOnOrAfter': All(str, _check_utc_date), }, 'children': {}, 'text': None, }, required=True, ) authn_context_class_ref = Schema( { 'attrs': {}, 'children': {}, 'text': All( str, In(SPID_LEVELS, msg=DEFAULT_LIST_VALUE_ERROR.format( ', '.join(SPID_LEVELS)))) }, required=True, ) requested_authn_context = Schema( { 'attrs': { 'Comparison': str }, 'children': { '{%s}AuthnContextClassRef' % (ASSERTION): authn_context_class_ref }, 'text': None }, required=True, ) scoping = Schema( { 'attrs': { 'ProxyCount': Equal('0', msg=DEFAULT_VALUE_ERROR.format('0')) }, 'children': {}, 'text': None }, required=True, ) signature = Schema( { 'attrs': dict, 'children': dict, 'text': None }, required=True, ) subject = Schema( { 'attrs': { 'Format': Equal( NAMEID_FORMAT_ENTITY, msg=DEFAULT_VALUE_ERROR.format(NAMEID_FORMAT_ENTITY)), 'NameQualifier': str }, 'children': {}, 'text': None }, required=True, ) # LOGIN def check_assertion_consumer_service(attrs): keys = attrs.keys() if ('AssertionConsumerServiceURL' in keys and 'ProtocolBinding' in keys and 'AssertionConsumerServiceIndex' not in keys): _errors = [] if attrs['ProtocolBinding'] != BINDING_HTTP_POST: _errors.append( Invalid(DEFAULT_VALUE_ERROR.format(BINDING_HTTP_POST), path=['ProtocolBinding'])) if attrs[ 'AssertionConsumerServiceURL'] not in assertion_consumer_service_urls: _errors.append( Invalid(DEFAULT_VALUE_ERROR.format( assertion_consumer_service_urls), path=['AssertionConsumerServiceURL'])) if _errors: raise MultipleInvalid(errors=_errors) return attrs elif ('AssertionConsumerServiceURL' not in keys and 'ProtocolBinding' not in keys and 'AssertionConsumerServiceIndex' in keys): if attrs[ 'AssertionConsumerServiceIndex'] not in assertion_consumer_service_indexes: raise Invalid(DEFAULT_LIST_VALUE_ERROR.format( ', '.join(assertion_consumer_service_indexes)), path=['AssertionConsumerServiceIndex']) return attrs else: raise Invalid( 'Uno e uno solo uno tra gli attributi o gruppi di attributi devono essere presenti: ' '[AssertionConsumerServiceIndex, [AssertionConsumerServiceUrl, ProtocolBinding]]' ) authnrequest_attr_schema = Schema(All( { 'ID': str, 'Version': Equal('2.0', msg=DEFAULT_VALUE_ERROR.format('2.0')), 'IssueInstant': All(str, _check_utc_date, self._check_date_in_range), 'Destination': Equal(entity_id, msg=DEFAULT_VALUE_ERROR.format(entity_id)), Optional('ForceAuthn'): str, Optional('AttributeConsumingServiceIndex'): In(attribute_consuming_service_indexes, msg=DEFAULT_LIST_VALUE_ERROR.format( ', '.join(attribute_consuming_service_indexes))), Optional('AssertionConsumerServiceIndex'): str, Optional('AssertionConsumerServiceURL'): str, Optional('ProtocolBinding'): str, }, check_assertion_consumer_service, ), required=True) AUTHNREQUEST_TAG = '{%s}AuthnRequest' % (PROTOCOL) authnrequest_schema = { AUTHNREQUEST_TAG: { 'attrs': authnrequest_attr_schema, 'children': Schema( { Optional('{%s}Subject' % (ASSERTION)): subject, '{%s}Issuer' % (ASSERTION): issuer, '{%s}NameIDPolicy' % (PROTOCOL): name_id_policy, Optional('{%s}Conditions' % (ASSERTION)): conditions, '{%s}RequestedAuthnContext' % (PROTOCOL): requested_authn_context, Optional('{%s}Scoping' % (PROTOCOL)): scoping, }, required=True, ), 'text': None } } # LOGOUT LOGOUTREQUEST_TAG = '{%s}LogoutRequest' % (PROTOCOL) logoutrequest_attr_schema = Schema(All({ 'ID': str, 'Version': Equal('2.0', msg=DEFAULT_VALUE_ERROR.format('2.0')), 'IssueInstant': All(str, _check_utc_date, self._check_date_in_range), 'Destination': Equal(entity_id, msg=DEFAULT_VALUE_ERROR.format(entity_id)), Optional('NotOnOrAfter'): All(str, _check_utc_date, self._check_date_not_expired), Optional('Reason'): str, }), required=True) logoutrequest_schema = { LOGOUTREQUEST_TAG: { 'attrs': logoutrequest_attr_schema, 'children': Schema( { '{%s}Issuer' % (ASSERTION): issuer, '{%s}NameID' % (ASSERTION): name_id, '{%s}SessionIndex' % (PROTOCOL): dict, }, required=True), 'text': None } } if self._binding == BINDING_HTTP_POST: if self._action == 'login': # Add signature schema _new_sub_schema = authnrequest_schema[AUTHNREQUEST_TAG][ 'children'].extend( {'{%s}Signature' % (SIGNATURE): signature}) authnrequest_schema[AUTHNREQUEST_TAG][ 'children'] = _new_sub_schema if self._action == 'logout': _new_sub_schema = logoutrequest_schema[LOGOUTREQUEST_TAG][ 'children'].extend( {'{%s}Signature' % (SIGNATURE): signature}) logoutrequest_schema[LOGOUTREQUEST_TAG][ 'children'] = _new_sub_schema authn_request = Schema( authnrequest_schema, required=True, ) logout_request = Schema( logoutrequest_schema, required=True, ) saml_schema = None if self._action == 'login': saml_schema = authn_request elif self._action == 'logout': saml_schema = logout_request errors = [] try: saml_schema(data) except MultipleInvalid as e: for err in e.errors: _paths = [] _attr = None for idx, _path in enumerate(err.path): if _path != 'children': if _path == 'attrs': try: _attr = err.path[(idx + 1)] except IndexError: _attr = '' break # strip namespaces for better readability _paths.append(_strip_namespaces(str(_path))) path = '/'.join(_paths) if _attr is not None: path += " - attribute: " + _attr # find value to show (iterate multiple times inside data # until we find the sub-element or attribute) _val = data for _ in err.path: try: _val = _val[_] except KeyError: _val = None except ValueError: _val = None # no need to show value if the error is the presence of the element _msg = err.msg if "extra keys not allowed" in _msg: _val = None _msg = "item not allowed" errors.append( ValidationDetail(_val, None, None, None, None, _msg, path)) raise SPIDValidationError(details=errors)
def validate(self, metadata): data = saml_to_dict(metadata) key_descriptor = Schema( All([{ 'attrs': Schema( { 'use': All( str, In(KEYDESCRIPTOR_USES, msg=DEFAULT_LIST_VALUE_ERROR.format( ', '.join(KEYDESCRIPTOR_USES)))), }, required=True), 'children': { '{%s}KeyInfo' % (SIGNATURE): { 'attrs': {}, 'children': { '{%s}X509Data' % (SIGNATURE): { 'attrs': {}, 'children': { '{%s}X509Certificate' % (SIGNATURE): { 'attrs': {}, 'children': {}, 'text': All(str, _check_certificate) } }, 'text': None } }, 'text': None } }, 'text': None }], self._check_keydescriptor), required=True, ) slo = Schema( All([{ 'attrs': dict, 'children': dict, 'text': None }], Length(min=1)), required=True, ) acs = Schema( All([{ 'attrs': dict, 'children': dict, 'text': None }], Length(min=1)), required=True, ) atcs = Schema( All([{ 'attrs': { 'index': str }, 'children': { '{%s}ServiceName' % (METADATA): { 'attrs': dict, 'children': {}, 'text': str }, Optional('{%s}ServiceDescription' % (METADATA)): { 'attrs': dict, 'children': {}, 'text': str }, '{%s}RequestedAttribute' % (METADATA): All([{ 'attrs': { 'Name': All( str, In(SPID_ATTRIBUTES_NAMES, msg=DEFAULT_LIST_VALUE_ERROR.format( ', '.join(SPID_ATTRIBUTES_NAMES)))), Optional('NameFormat'): Equal(NAME_FORMAT_BASIC, msg=DEFAULT_VALUE_ERROR.format( NAME_FORMAT_BASIC)), Optional('FriendlyName'): str, Optional('isRequired'): str }, 'children': {}, 'text': None }], Length(min=1)), }, 'text': None }], Length(min=1)), required=True, ) name_id_format = Schema( { 'attrs': {}, 'children': {}, 'text': Equal(NAMEID_FORMAT_TRANSIENT, msg=DEFAULT_VALUE_ERROR.format(NAMEID_FORMAT_TRANSIENT)), }, required=True, ) spsso_descriptor_attr_schema = Schema(All({ 'protocolSupportEnumeration': Equal(PROTOCOL, msg=DEFAULT_VALUE_ERROR.format(PROTOCOL)), 'AuthnRequestsSigned': Equal('true', msg=DEFAULT_VALUE_ERROR.format('true')), Optional('WantAssertionsSigned'): str, }), required=True) spsso = Schema( { 'attrs': spsso_descriptor_attr_schema, 'children': { '{%s}KeyDescriptor' % (METADATA): key_descriptor, '{%s}SingleLogoutService' % (METADATA): slo, '{%s}AssertionConsumerService' % (METADATA): acs, '{%s}AttributeConsumingService' % (METADATA): atcs, '{%s}NameIDFormat' % (METADATA): name_id_format, }, 'text': None }, required=True) entity_descriptor_schema = Schema( { '{%s}EntityDescriptor' % (METADATA): { 'attrs': Schema( { 'entityID': str, Optional('ID'): str, Optional('validUntil'): All(str, _check_utc_date), Optional('cacheDuration'): str, Optional('Name'): str, }, required=True), 'children': Schema( { Optional('{%s}Signature' % (SIGNATURE)): Schema( { 'attrs': dict, 'children': dict, 'text': None }, required=True, ), '{%s}SPSSODescriptor' % (METADATA): spsso, Optional('{%s}Organization' % (METADATA)): dict, Optional('{%s}ContactPerson' % (METADATA)): list }, required=True), 'text': None } }, required=True) errors = [] try: entity_descriptor_schema(data) except MultipleInvalid as e: for err in e.errors: _val = data _paths = [] _attr = None for idx, _path in enumerate(err.path): if _path != 'children': if _path == 'attrs': try: _attr = err.path[(idx + 1)] except IndexError: _attr = '' break # strip namespaces for better readability _paths.append(_strip_namespaces(str(_path))) path = '/'.join(_paths) if _attr is not None: path = '{} - attribute: {}'.format(path, _attr) for _ in err.path: try: _val = _val[_] except IndexError: _val = None except KeyError: _val = None errors.append( ValidationDetail(_val, None, None, None, None, err.msg, path)) raise SPIDValidationError(details=errors)
def validate(self, request): xmlstr = request.saml_request data = saml_to_dict(xmlstr) atcss = [] if self._action == 'login': req_type = 'AuthnRequest' service = 'single_sign_on_service' elif self._action == 'logout': req_type = 'LogoutRequest' service = 'single_logout_service' issuer_name = data.get( '{urn:oasis:names:tc:SAML:2.0:protocol}%s' % (req_type), {}).get('children', {}).get('{urn:oasis:names:tc:SAML:2.0:assertion}Issuer', {}).get('text') if issuer_name and issuer_name not in self._metadata.service_providers( ): raise UnknownEntityIDError( 'entity ID {} non registrato'.format(issuer_name)) for k, _md in self._metadata.items(): if k == issuer_name: _srvs = _md.get('spsso_descriptor', []) for _srv in _srvs: for _acs in _srv.get('attribute_consuming_service', []): atcss.append(_acs) try: ascss = self._metadata.assertion_consumer_service(issuer_name) except Exception: ascss = [] except Exception: ascss = [] attribute_consuming_service_indexes = [ str(el.get('index')) for el in atcss ] assertion_consumer_service_indexes = [ str(el.get('index')) for el in ascss ] receivers = self._config.endpoint(service, self._binding, 'idp') issuer = Schema( { 'attrs': { 'Format': Equal( NAMEID_FORMAT_ENTITY, msg=DEFAULT_VALUE_ERROR.format(NAMEID_FORMAT_ENTITY)), 'NameQualifier': Equal(issuer_name, msg=DEFAULT_VALUE_ERROR.format(issuer_name)), }, 'children': {}, 'text': str, }, required=True, ) name_id = Schema( { 'attrs': { 'NameQualifier': str, 'Format': Equal(NAMEID_FORMAT_TRANSIENT, msg=DEFAULT_VALUE_ERROR.format( NAMEID_FORMAT_TRANSIENT)), }, 'children': {}, 'text': str }, required=True, ) name_id_policy = Schema( { 'attrs': { 'Format': Equal(NAMEID_FORMAT_TRANSIENT, msg=DEFAULT_VALUE_ERROR.format( NAMEID_FORMAT_TRANSIENT)), }, 'children': {}, 'text': None, }, required=True, ) conditions = Schema( { 'attrs': { 'NotBefore': All(str, self._check_utc_date), 'NotOnOrAfter': All(str, self._check_utc_date), }, 'children': {}, 'text': None, }, required=True, ) authn_context_class_ref = Schema( { 'attrs': {}, 'children': {}, 'text': All( str, In(SPID_LEVELS, msg=DEFAULT_LIST_VALUE_ERROR.format(SPID_LEVELS))) }, required=True, ) requested_authn_context = Schema( { 'attrs': { 'Comparison': str }, 'children': { '{%s}AuthnContextClassRef' % (ASSERTION): authn_context_class_ref }, 'text': None }, required=True, ) scoping = Schema( { 'attrs': { 'ProxyCount': Equal('0', msg=DEFAULT_VALUE_ERROR.format('0')) }, 'children': {}, 'text': None }, required=True, ) signature = Schema( { 'attrs': dict, 'children': dict, 'text': None }, required=True, ) subject = Schema( { 'attrs': { 'Format': Equal( NAMEID_FORMAT_ENTITY, msg=DEFAULT_VALUE_ERROR.format(NAMEID_FORMAT_ENTITY)), 'NameQualifier': str }, 'children': {}, 'text': None }, required=True, ) # LOGIN def check_assertion_consumer_service(attrs): keys = attrs.keys() if ('AssertionConsumerServiceURL' in keys and 'ProtocolBinding' in keys and 'AssertionConsumerServiceIndex' not in keys): if attrs['ProtocolBinding'] != BINDING_HTTP_POST: raise Invalid( DEFAULT_VALUE_ERROR.format(BINDING_HTTP_POST), path=['ProtocolBinding']) return attrs elif ('AssertionConsumerServiceURL' not in keys and 'ProtocolBinding' not in keys and 'AssertionConsumerServiceIndex' in keys): if attrs[ 'AssertionConsumerServiceIndex'] not in assertion_consumer_service_indexes: raise Invalid(DEFAULT_LIST_VALUE_ERROR.format( assertion_consumer_service_indexes), path=['AssertionConsumerServiceIndex']) return attrs else: raise Invalid( 'Uno e uno solo uno tra gli attributi o gruppi di attributi devono essere presenti: ' '[AssertionConsumerServiceIndex, [AssertionConsumerServiceUrl, ProtocolBinding]]' ) authnrequest_attr_schema = Schema(All( { 'ID': str, 'Version': Equal('2.0', msg=DEFAULT_VALUE_ERROR.format('2.0')), 'IssueInstant': All(str, self._check_utc_date, self._check_date_in_range), 'Destination': In(receivers, msg=DEFAULT_LIST_VALUE_ERROR.format(receivers)), Optional('ForceAuthn'): str, Optional('AttributeConsumingServiceIndex'): In(attribute_consuming_service_indexes, msg=DEFAULT_LIST_VALUE_ERROR.format( attribute_consuming_service_indexes)), Optional('AssertionConsumerServiceIndex'): str, Optional('AssertionConsumerServiceURL'): str, Optional('ProtocolBinding'): str, }, check_assertion_consumer_service, ), required=True) authnrequest_schema = { '{%s}AuthnRequest' % (PROTOCOL): { 'attrs': authnrequest_attr_schema, 'children': Schema( { Optional('{%s}Subject' % (ASSERTION)): subject, '{%s}Issuer' % (ASSERTION): issuer, '{%s}NameIDPolicy' % (PROTOCOL): name_id_policy, Optional('{%s}Conditions' % (ASSERTION)): conditions, '{%s}RequestedAuthnContext' % (PROTOCOL): requested_authn_context, Optional('{%s}Scoping' % (PROTOCOL)): scoping, }, required=True, ), 'text': None } } if self._binding == BINDING_HTTP_POST: authnrequest_schema['{%s}AuthnRequest' % (PROTOCOL)]['children'].extend = { '{%s}Signature' % (SIGNATURE): signature } authn_request = Schema( authnrequest_schema, required=True, ) # LOGOUT logout_request = Schema( { '{%s}LogoutRequest' % (PROTOCOL): { 'attrs': { 'ID': str, 'Version': Equal('2.0', msg=DEFAULT_VALUE_ERROR.format('2.0')), 'IssueInstant': All(str, self._check_utc_date, self._check_date_in_range), 'Destination': In(receivers, msg=DEFAULT_LIST_VALUE_ERROR.format(receivers)), }, 'children': { '{%s}Issuer' % (ASSERTION): issuer, '{%s}NameID' % (ASSERTION): name_id, '{%s}SessionIndex' % (PROTOCOL): dict, }, 'text': None } }, required=True, ) saml_schema = None if self._action == 'login': saml_schema = authn_request elif self._action == 'logout': saml_schema = logout_request errors = [] try: saml_schema(data) except MultipleInvalid as e: for err in e.errors: _val = data _paths = [] _attr = None for idx, _path in enumerate(err.path): if _path != 'children': if _path == 'attrs': try: _attr = err.path[(idx + 1)] except IndexError: _attr = '' break _paths.append(_path) path = '/'.join(_paths) path = 'xpath: {}'.format(path) if _attr is not None: path = '{} - attribute: {}'.format(path, _attr) for _ in err.path: _val = _val.get(_) errors.append( ValidationDetail(_val, None, None, None, None, err.msg, path)) raise SPIDValidationError(details=errors)