def validate_fields(self,
                        required_type: Type,
                        *fields: str,
                        required: bool = True) -> None:
        """
        Validate fields.

        :param required_type: The required type of the field.
        :param fields: The fields to validate.
        :param required: Whether the field is required or can be None.
        :raise ValidationError: when validation fails.
        """
        for name in fields:
            value = getattr(self, name)
            if isinstance(value, str) and not value:
                value = None  # Treat empty strings as None
            if not isinstance(value, required_type):
                if required:
                    raise ValidationError({
                        name:
                        'Must be {}, not {}.'.format(required_type.__name__,
                                                     type(value).__name__)
                    })
                if value is not None:
                    raise ValidationError({
                        name:
                        'Must be {} or None, not {}.'.format(
                            required_type.__name__,
                            type(value).__name__)
                    })
    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 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 validate_attributes(model: DataModel, field_name: str) -> None:
    """Validate eIDAS attributes."""
    model.validate_fields(dict, field_name, required=True)
    attributes = getattr(model, field_name)  # type: Dict[str, List[str]]
    for key, values in attributes.items():
        if not isinstance(key, str) or not key.strip():
            raise ValidationError({field_name: 'All keys must be strings.'})
        if not isinstance(values, list) or any(not isinstance(value, str)
                                               for value in values):
            raise ValidationError(
                {field_name: 'All values must be lists of strings.'})
Esempio n. 5
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
 def validate(self) -> None:
     """Validate this data model."""
     self.validate_fields(str, 'id', 'issuer', required=True)
     self.validate_fields(datetime, 'created', required=True)
     for field in 'id', 'issuer':
         if '|' in getattr(self, field):
             raise ValidationError({field: 'Character "|" not allowed.'})
Esempio n. 7
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
Esempio n. 8
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
Esempio n. 9
0
 def test_errors_attribute(self):
     self.assertIs(ValidationError(self.ERRORS).errors, self.ERRORS)
Esempio n. 10
0
 def test_str(self):
     self.assertEqual(str(ValidationError(self.ERRORS)), "Validation failed: {'name': 'Invalid name.'}")
Esempio n. 11
0
 def test_repr(self):
     self.assertEqual(repr(ValidationError(self.ERRORS)), "ValidationError({'name': 'Invalid name.'})")
Esempio n. 12
0
    def from_light_request(cls: Type[SAMLRequestType],
                           light_request: LightRequest, destination: str,
                           issued: datetime) -> SAMLRequestType:
        """
        Convert Light Request to SAML Request.

        :param light_request: The light request to convert.
        :param destination: A URI reference indicating the address to which this request has been sent.
        :param issued: The UTC time instant of issue of the request.
        :return: A SAML Request.
        """
        light_request.validate()
        if not is_xml_id_valid(light_request.id):
            raise ValidationError({
                'id':
                'Light request id is not a valid XML id: {!r}'.format(
                    light_request.id)
            })

        root_attributes = OrderedDict([
            ('Consent', 'urn:oasis:names:tc:SAML:2.0:consent:unspecified'
             ),  # optional, default 'unspecified'
            ('Destination', destination),
            ('ID', light_request.id),
            ('IssueInstant',
             datetime_iso_format_milliseconds(issued) + 'Z'),  # UTC
            ('Version', '2.0'),
            ('IsPassive', 'false'),  # optional, default false
            ('ForceAuthn', 'true'),  # optional, default false
        ])
        if light_request.provider_name is not None:
            root_attributes['ProviderName'] = light_request.provider_name
        root = etree.Element(Q_NAMES['saml2p:AuthnRequest'],
                             attrib=root_attributes,
                             nsmap=EIDAS_NAMESPACES)

        # 1. RequestAbstractType <saml2:Issuer>:
        if light_request.issuer is not None:
            SubElement(root, Q_NAMES['saml2:Issuer'], {
                'Format':
                'urn:oasis:names:tc:SAML:2.0:nameid-format:entity'
            }).text = light_request.issuer

        # 2. RequestAbstractType <ds:Signature> skipped
        # 3. RequestAbstractType <saml2p:Extensions>:
        extensions = SubElement(root, Q_NAMES['saml2p:Extensions'])
        if light_request.sp_type:
            SubElement(
                extensions,
                Q_NAMES['eidas:SPType']).text = light_request.sp_type.value
        # In patched version 2.3.1 of CEF eIDAS Node (according EID-922), origin_country_code is used
        if light_request.sp_country_code:
            SubElement(extensions, Q_NAMES['eidas:SPCountry']
                       ).text = light_request.sp_country_code
        # In the version 2.4 of CEF eIDAS Node, citizen_country_code is used
        else:
            assert light_request.citizen_country_code  # mandatory field
            SubElement(extensions, Q_NAMES['eidas:SPCountry']
                       ).text = light_request.citizen_country_code
        attributes = SubElement(extensions,
                                Q_NAMES['eidas:RequestedAttributes'])
        for name, values in light_request.requested_attributes.items():
            attribute = SubElement(attributes,
                                   Q_NAMES['eidas:RequestedAttribute'],
                                   create_attribute_elm_attributes(name, True))
            for value in values:
                SubElement(attribute,
                           Q_NAMES['eidas:AttributeValue']).text = value

        # 4. AuthnRequestType <saml2:Subject> skipped
        # 5. AuthnRequestType <saml2p:NameIDPolicy>:
        if light_request.name_id_format:
            SubElement(
                root,
                Q_NAMES['saml2p:NameIDPolicy'],
                {
                    'AllowCreate': 'true',  # optional, default false
                    'Format': light_request.name_id_format.value
                })
        # 6. AuthnRequestType <saml2:Conditions> skipped
        # 7. AuthnRequestType <saml2p:RequestedAuthnContext>:
        SubElement(
            SubElement(root, Q_NAMES['saml2p:RequestedAuthnContext'],
                       {'Comparison': 'minimum'}),
            Q_NAMES['saml2:AuthnContextClassRef']
        ).text = light_request.level_of_assurance.value
        # 8: AuthnRequestType <saml2p:Scoping> skipped
        return cls(ElementTree(root), light_request.citizen_country_code,
                   light_request.relay_state)