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
Example #4
0
    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
Example #5
0
    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
Example #6
0
    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>')