def load_xml(cls: Type[T], root: Union[Element, ElementTree]) -> T: """ Load Light Request from a XML document. :param root: The XML document to load. :raise ValidationError: If the XML document does not have a valid schema. :raise TypeError: If ROOT_ELEMENT class attribute is not defined. """ if not cls.ROOT_ELEMENT: raise TypeError('XMLDataModel subclasses must define ROOT_ELEMENT class attribute.') if hasattr(root, 'getroot'): root = root.getroot() if QName(root.tag).localname != cls.ROOT_ELEMENT: raise ValidationError({get_element_path(root): 'Invalid root element {!r}.'.format(root.tag)}) model = cls() for elm in root: field_name = convert_tag_name_to_field_name(elm.tag) if field_name not in cls.FIELDS: raise ValidationError({get_element_path(elm): 'Unknown element {!r}.'.format(elm.tag)}) deserialize_func = getattr(model, 'deserialize_' + field_name, None) setattr(model, field_name, deserialize_func(elm) if deserialize_func else elm.text) return model
def test_get_element_path_mixed(self): root = Element(Q_NAMES['saml2p:Response'], nsmap=EIDAS_NAMESPACES) leaf = SubElement( SubElement(root, Q_NAMES['saml2:EncryptedAssertion']), 'wrong') self.assertEqual(get_element_path(root), '<saml2p:Response>') self.assertEqual(get_element_path(leaf), '<saml2p:Response><saml2:EncryptedAssertion><wrong>')
def deserialize_attributes(attributes_elm: Element) -> Dict[str, List[str]]: """Deserialize eIDAS attributes.""" attributes = OrderedDict() # type: Dict[str, List[str]] for attribute in attributes_elm: if QName(attribute.tag).localname != 'attribute': raise ValidationError({ get_element_path(attribute): 'Unexpected element {!r}'.format(attribute.tag) }) if not len(attribute): raise ValidationError({ get_element_path(attribute): 'Missing attribute.definition element.' }) definition = attribute[0] if QName(definition.tag).localname != 'definition': raise ValidationError({ get_element_path(definition): 'Unexpected element {!r}'.format(definition.tag) }) values = attributes[definition.text] = [] for value in attribute[1:]: if QName(value.tag).localname != 'value': raise ValidationError({ get_element_path(value): 'Unexpected element {!r}'.format(value.tag) }) values.append(value.text) return attributes
def create_light_request(self) -> LightRequest: """ Convert SAML Request to Light Request. :return: A Light Request. :raise ValidationError: If the SAML Request cannot be parsed correctly. """ request = LightRequest(requested_attributes=OrderedDict(), citizen_country_code=self.citizen_country_code, relay_state=self.relay_state) root = self.document.getroot() if root.tag != Q_NAMES['saml2p:AuthnRequest']: raise ValidationError({get_element_path(root): 'Wrong root element: {!r}'.format(root.tag)}) request.id = root.attrib.get('ID') request.provider_name = root.attrib.get('ProviderName') request.issuer = self.issuer name_id_format = root.find('./{}'.format(Q_NAMES['saml2p:NameIDPolicy'])) if name_id_format is not None: request.name_id_format = NameIdFormat(name_id_format.attrib.get('Format')) level_of_assurance = root.find( './{}/{}'.format(Q_NAMES['saml2p:RequestedAuthnContext'], Q_NAMES['saml2:AuthnContextClassRef'])) if level_of_assurance is not None: request.level_of_assurance = LevelOfAssurance(level_of_assurance.text) extensions = root.find('./{}'.format(Q_NAMES['saml2p:Extensions'])) if extensions is not None: sp_type = extensions.find('./{}'.format(Q_NAMES['eidas:SPType'])) if sp_type is not None: request.sp_type = ServiceProviderType(sp_type.text) sp_country = extensions.find('./{}'.format(Q_NAMES['eidas:SPCountry'])) if sp_country is not None: request.origin_country_code = sp_country.text requested_attributes = request.requested_attributes attributes = extensions.findall( './{}/{}'.format(Q_NAMES['eidas:RequestedAttributes'], Q_NAMES['eidas:RequestedAttribute'])) for attribute in attributes: name = attribute.attrib.get('Name') if not name: raise ValidationError({ get_element_path(attribute): "Missing attribute 'Name'"}) values = requested_attributes[name] = [] for value in attribute.findall('./{}'.format(Q_NAMES['eidas:AttributeValue'])): values.append(value.text) return request
def _parse_assertion(self, response: LightResponse, assertion: Element, auth_class_map: Optional[Dict[str, LevelOfAssurance]]) -> None: attributes = response.attributes = OrderedDict() name_id_elm = assertion.find('./{}/{}'.format(Q_NAMES['saml2:Subject'], Q_NAMES['saml2:NameID'])) if name_id_elm is not None: response.subject = name_id_elm.text response.subject_name_id_format = NameIdFormat(name_id_elm.get('Format')) attribute_elms = assertion.findall('./{}/{}'.format(Q_NAMES['saml2:AttributeStatement'], Q_NAMES['saml2:Attribute'])) for attribute in attribute_elms: attributes[attribute.get('Name')] = [ elm.text for elm in attribute.findall('./{}'.format(Q_NAMES['saml2:AttributeValue']))] stm_elm = assertion.find('./{}'.format(Q_NAMES['saml2:AuthnStatement'])) if stm_elm is not None: locality_elm = stm_elm.find('./{}'.format(Q_NAMES['saml2:SubjectLocality'])) if locality_elm is not None: response.ip_address = locality_elm.get('Address') authn_class_elm = stm_elm.find('./{}/{}'.format(Q_NAMES['saml2:AuthnContext'], Q_NAMES['saml2:AuthnContextClassRef'])) if authn_class_elm is not None: try: response.level_of_assurance = LevelOfAssurance(authn_class_elm.text) except ValueError: if auth_class_map and authn_class_elm.text in auth_class_map: response.level_of_assurance = auth_class_map[authn_class_elm.text] else: raise ValidationError({get_element_path(authn_class_elm): authn_class_elm.text}) from None
def create_light_response(self, auth_class_map: Dict[str, LevelOfAssurance] = None) -> LightResponse: """Convert SAML response to light response.""" response = LightResponse(attributes=OrderedDict()) root = self.document.getroot() if root.tag != Q_NAMES['saml2p:Response']: raise ValidationError({ get_element_path(root): 'Wrong root element: {!r}'.format(root.tag)}) response.id = root.get('ID') response.in_response_to_id = root.get('InResponseTo') response.issuer = self.issuer response.status = status = Status() status_code_elm = root.find('./{}/{}'.format(Q_NAMES['saml2p:Status'], Q_NAMES['saml2p:StatusCode'])) if status_code_elm is not None: sub_status_code_elm = status_code_elm.find('./{}'.format(Q_NAMES['saml2p:StatusCode'])) status_message_elm = root.find('./{}/{}'.format(Q_NAMES['saml2p:Status'], Q_NAMES['saml2p:StatusMessage'])) status_code = status_code_elm.get('Value') sub_status_code = sub_status_code_elm.get('Value') if sub_status_code_elm is not None else None if status_code == SubStatusCode.VERSION_MISMATCH.value: # VERSION_MISMATCH is a status code in SAML 2 but a sub status code in Light response! status.status_code = StatusCode.REQUESTER status.sub_status_code = SubStatusCode.VERSION_MISMATCH else: status.status_code = StatusCode(status_code) try: status.sub_status_code = SubStatusCode(sub_status_code) except ValueError: # None or a sub status codes not recognized by eIDAS status.sub_status_code = None status.failure = status.status_code != StatusCode.SUCCESS if status_message_elm is not None: status.status_message = status_message_elm.text assertion_elm = self.assertion if assertion_elm is not None: try: self._parse_assertion(response, assertion_elm, auth_class_map) except ValidationError as e: return LightResponse( id=root.get('ID'), in_response_to_id=root.get('InResponseTo'), issuer=self.issuer, status=Status( failure=True, status_code=StatusCode.RESPONDER, status_message='Invalid data of {!r}: {!r}'.format(*e.errors.popitem()))) response.relay_state = self.relay_state return response
def test_get_element_path_with_namespaces(self): root = Element(Q_NAMES['saml2p:Response'], nsmap=EIDAS_NAMESPACES) leaf = SubElement(root, Q_NAMES['saml2:EncryptedAssertion']) self.assertEqual(get_element_path(root), '<saml2p:Response>') self.assertEqual(get_element_path(leaf), '<saml2p:Response><saml2:EncryptedAssertion>')
def test_get_element_path_without_namespaces(self): root = Element('root') grandchild = SubElement(SubElement(root, 'child'), 'grandchild') self.assertEqual(get_element_path(root), '<root>') self.assertEqual(get_element_path(grandchild), '<root><child><grandchild>')