예제 #1
0
def xml_to_str(tree, encoding=None, xml_declaration=False):
    """Serialize an XML tree. Returns unicode if 'encoding' is None. Otherwise, we return encoded 'bytes'."""
    if xml_declaration and not encoding:
        raise ValueError("'xml_declaration' is not supported when 'encoding' is None")
    if encoding:
        return tostring(tree, encoding=encoding, xml_declaration=True)
    return tostring(tree, encoding=text_type, xml_declaration=False)
예제 #2
0
def xml_to_str(tree, encoding=None, xml_declaration=False):
    """Serialize an XML tree. Returns unicode if 'encoding' is None. Otherwise, we return encoded 'bytes'."""
    if xml_declaration and not encoding:
        raise ValueError("'xml_declaration' is not supported when 'encoding' is None")
    if encoding:
        return tostring(tree, encoding=encoding, xml_declaration=True)
    return tostring(tree, encoding=text_type, xml_declaration=False)
예제 #3
0
def xpath_selector(selector, html, select_all):
    """
    Returns Xpath match for `selector` within `html`.

    :param selector: XPath string
    :param html: Unicode content
    :param select_all: True to get all matches
    """
    from defusedxml import lxml as dlxml
    from lxml import etree
    import re

    # lxml requires argument to be bytes
    # see https://github.com/kibitzr/kibitzr/issues/47
    encoded = html.encode('utf-8')
    root = dlxml.fromstring(encoded, parser=etree.HTMLParser())
    elements = root.xpath(selector)
    if not elements:
        logger.warning('XPath selector not found: %r', selector)
        return False, html
    if select_all is False:
        elements = elements[0:1]

    elements = [
        re.sub(r'\s+', ' ',
               dlxml.tostring(ele, method='html', encoding='unicode')).strip()
        for ele in elements
    ]

    return True, u"\n".join(six.text_type(x) for x in elements)
예제 #4
0
    def __str__(self, level=0):
        """

        Parameters
        ----------
        level : int
                Number of double spaces "  " to indent the resulting output to

        Returns
        -------
        str representation of this element, pretty print of entire contents.
        """
        if self.text:
            cur_node = xml_node(self.tag, self.text)
            result = "{}{}".format(
                "  " * level,
                lxml.tostring(cur_node, pretty_print=True).decode())
            result = result.rstrip()
        else:
            result = "{}<{}>".format("  " * level, self.tag, self.tag)
            for child in self.children:
                if type(self.__dict__[child.tag]) == XMLNode:
                    child = self.__dict__[child.tag]
                result += '\n' + child.__str__(level=level + 1)
            result += '\n{}</{}>'.format("  " * level, self.tag)
        return result
예제 #5
0
파일: util.py 프로젝트: tobpe/exchangelib
def to_xml(text):
    try:
        if PY2:
            # On python2, fromstring expects an encoded string
            return fromstring((text[BOM_LEN:] if text.startswith(BOM) else text).encode('utf-8'))
        return fromstring(text[BOM_LEN:] if text.startswith(BOM) else text)
    except ParseError:
        # Exchange servers may spit out the weirdest XML. lxml is pretty good at recovering from errors
        log.warning('Fallback to lxml processing of faulty XML')
        magical_parser = XMLParser(recover=True, resolve_entities=False)
        magical_parser.set_element_class_lookup(ElementDefaultClassLookup(element=RestrictedElement))
        no_bom_text = text[BOM_LEN:] if text.startswith(BOM) else text
        try:
            root = parse(io.BytesIO(no_bom_text.encode('utf-8')), parser=magical_parser)
        except AssertionError as e:
            raise ParseError(*e.args)
        try:
            return fromstring(tostring(root))
        except ParseError as e:
            if hasattr(e, 'position'):
                e.lineno, e.offset = e.position
            if not e.lineno:
                raise ParseError('%s' % text_type(e))
            try:
                offending_line = no_bom_text.splitlines()[e.lineno - 1]
            except IndexError:
                raise ParseError('%s' % text_type(e))
            else:
                offending_excerpt = offending_line[max(0, e.offset - 20):e.offset + 20]
                raise ParseError('%s\nOffending text: [...]%s[...]' % (text_type(e), offending_excerpt))
        except TypeError:
            raise ParseError('This is not XML: %s' % text)
    def handle(self, *args, **options):
        client = get_soap_client()

        if not options['command']:
            self.logger.info('No command specified, dumping wsdl information')
            client.wsdl.dump()
            return

        arguments = {
            x.split('=')[0]: x.split('=')[1]
            for x in options["arguments"]
        }
        self.logger.info(
            f'Calling command {options["command"]} with parameters {arguments}'
        )
        response = getattr(client.service, options['command'])(**arguments)
        dict_response = xml_etree_to_dict(response)
        self.logger.info(type(response))
        if not isinstance(response, str):
            response = ET.tostring(response, pretty_print=True)

        self.logger.info(f'SOAP call response:')
        self.logger.info(response.decode('unicode_escape'))

        self.logger.info(f'Dictionary parsed response: {dict_response}')
예제 #7
0
def node_to_string(node, encoding=True):
    """

    Parameters
    ----------
    node : lxml note

    Returns
    -------

    str :
    Pretty string representation of node
    """
    if not type(node) == etree._ElementTree:
        tree = etree.ElementTree(node)
    else:
        tree = node

    return lxml.tostring(
        tree,
        pretty_print=True,
        with_tail=False,
        encoding="UTF-8",
        xml_declaration=encoding,
    ).decode("utf-8")
예제 #8
0
    def xml_tree(self):
        """
        Parse the infile with lxml and add the proper namespace if required.

        :return etree.ElementTree: An lxml ElementTree with proper namespace
        """
        if hasattr(self.infile, 'seek'):
            self.infile.seek(0)

        tree = lxml.parse(self.infile)

        if self.meta.namespaces:
            return tree

        log.debug('Adding namespaces to xml for validation')
        root = tree.getroot()
        ns_root = etree.Element(
            tree.docinfo.root_name,
            root.attrib,
            nsmap={None: self.meta.get_ns_string()}
        )
        ns_root[:] = root[:]

        # Roundtrip to add namespace
        doc = lxml.tostring(
            ns_root,
            encoding=tree.docinfo.encoding,
            xml_declaration=True,
            pretty_print=True
        )
        ns_tree = lxml.fromstring(doc)
        return etree.ElementTree(ns_tree)
예제 #9
0
def serialize_xpath_results(xpath_results, select_all):
    """
    Serializes results of xpath evaluation.

    :param xpath_results: Results of xpath evaluation.
    See: https://lxml.de/xpathxslt.html#xpath-return-values

    :param select_all: True to get all matches
    """
    from defusedxml import lxml as dlxml
    import re

    if isinstance(xpath_results, list):
        if select_all is False:
            xpath_results = xpath_results[0:1]
    else:
        xpath_results = [xpath_results]

    results = []
    for r in xpath_results:
        # namespace declarations
        if isinstance(r, tuple):
            results.append("%s=\"%s\"" % (r[0], r[1]))
        # an element
        elif hasattr(r, 'tag'):
            results.append(
                re.sub(r'\s+', ' ',
                       dlxml.tostring(r, method='html', encoding='unicode'))
            )
        else:
            results.append(r)

    return u"\n".join(six.text_type(x).strip() for x in results)
예제 #10
0
 def prettify_xml(cls, xml_bytes):
     # Re-formats an XML document to a consistent style
     return tostring(cls.parse_bytes(xml_bytes),
                     xml_declaration=True,
                     encoding='utf-8',
                     pretty_print=True).replace(b'\t', b'    ').replace(
                         b' xmlns:', b'\n    xmlns:')
예제 #11
0
파일: models.py 프로젝트: titusz/onixcheck
    def xml_tree(self):
        """
        Parse the infile with lxml and add the proper namespace if required.

        :return etree.ElementTree: An lxml ElementTree with proper namespace
        """
        if hasattr(self.infile, 'seek'):
            self.infile.seek(0)

        tree = lxml.parse(self.infile)

        if self.meta.namespaces:
            return tree

        log.debug('Adding namespaces to xml for validation')
        root = tree.getroot()
        ns_root = etree.Element(
            tree.docinfo.root_name,
            root.attrib,
            nsmap={None: self.meta.get_ns_string()}
        )
        ns_root[:] = root[:]

        # Roundtrip to add namespace
        doc = lxml.tostring(
            ns_root,
            encoding=tree.docinfo.encoding,
            xml_declaration=True,
            pretty_print=True
        )
        ns_tree = lxml.fromstring(doc)
        return etree.ElementTree(ns_tree)
예제 #12
0
파일: html.py 프로젝트: niklas-heer/kibitzr
def xpath_selector(selector, html):
    """
    Returns Xpath match for `selector` within `html`.

    :param selector: XPath string
    :param html: Unicode content
    """
    from defusedxml import lxml as dlxml
    from lxml import etree

    # lxml requires argument to be bytes
    # see https://github.com/kibitzr/kibitzr/issues/47
    encoded = html.encode('utf-8')
    root = dlxml.fromstring(encoded, parser=etree.HTMLParser())
    elements = root.xpath(selector)
    if elements:
        return True, dlxml.tostring(
            next(iter(elements)),
            method='html',
            pretty_print=True,
            encoding='unicode',
        )
    else:
        logger.warning('XPath selector not found: %r', selector)
        return False, html
예제 #13
0
파일: api.py 프로젝트: RWTH-IAEW/cimpyorm
def export(dataset, mode: str = "Single",
           profile_whitelist: Union[list, tuple] = None,
           header_data: dict = None):
    """
    Returns a XML-serialization of the dataset within a zip-Archive.

    :param dataset: The dataset/SQLAlchemy-Session object to export.
    :param mode: {'Single', 'Multi'} - The export Mode, e.g. Single-File or Split by profiles.
    :param profile_whitelist: The profiles to export. Mandatory if Export-mode is 'Multi'.
        Profiles can be specificed by their full name (e.g. 'EquipmentProfile' or their short name 'EQ')
    :param header_data: Additional information for creating the file-headers (FullModel Objects).
        This is a dictionary of CIM-header supplements. Currently only the 'profile_header'
        field is supported, in which a list of profile identifiers (e.g.
        'http://entsoe.eu/CIM/Topology/4/1') are defined.

    :return: A BytesIO-filehandle containing a zip-archive of the export.
    """
    if not mode in ("Multi", "Single"):
        raise ValueError("Unknown serialization mode ('Single' and 'Multi' are valid values)")
    trees = serialize(dataset, mode, profile_whitelist, header_data)
    if isinstance(trees, _ElementTree):
        trees = [trees]
    file = BytesIO()

    with ZipFile(file, "w") as zf:
        if not profile_whitelist or mode == "Single":
            if not len(trees) == 1:
                raise ValueError("Too many objects returned by serializer.")
            zf.writestr(
                f"Export.xml",
                tostring(trees[0],
                         encoding="UTF-8",
                         xml_declaration=True,
                         pretty_print=True)
            )
        else:
            profile_lib = {p.name: p for p in dataset.query(CIMProfile)}
            for profile, tree in zip(profile_whitelist, trees):
                fname = profile_lib[profile].short
                zf.writestr(
                    f"{fname}.xml",
                    tostring(tree,
                             encoding="UTF-8",
                             xml_declaration=True,
                             pretty_print=True)
                )
    return file
예제 #14
0
 def prettify_xml(cls, xml_bytes):
     # Re-formats an XML document to a consistent style
     return tostring(
         cls.parse_bytes(xml_bytes),
         xml_declaration=True,
         encoding='utf-8',
         pretty_print=True
     ).replace(b'\t', b'    ').replace(b' xmlns:', b'\n    xmlns:')
예제 #15
0
 def process(value):
     if value is not None:
         if isinstance(value, str):
             return value
         else:
             return tostring(value)
     else:
         return None
예제 #16
0
def add_sign(xml, key, cert, debug=False):
    """Add sign.

    Args:
        xml (str): SAML assertion
        key (Path): path enc key
        cert (Path): path to cert/pem
        debug (boolean): xmlsec enable debug trace

    Returns:
        str: signed SAML assertion

    Raises:
        Exception: if xml is empty
    """
    if xml is None or xml == '':
        raise Exception('Empty string supplied as input')

    elem = fromstring(xml.encode('utf-8'), forbid_dtd=True)

    sign_algorithm_transform = xmlsec.Transform.ECDSA_SHA256

    signature = xmlsec.template.create(
        elem, xmlsec.Transform.EXCL_C14N, sign_algorithm_transform, ns='ds',
    )

    issuer = elem.findall('.//{urn:oasis:names:tc:SAML:2.0:assertion}Issuer')
    if issuer:
        issuer = issuer[0]
        issuer.addnext(signature)
        elem_to_sign = issuer.getparent()

    elem_id = elem_to_sign.get('ID', None)
    if elem_id is not None:
        if elem_id:
            elem_id = f'#{elem_id}'

    xmlsec.enable_debug_trace(debug)
    xmlsec.tree.add_ids(elem_to_sign, ['ID'])

    digest_algorithm_transform = xmlsec.Transform.SHA256

    ref = xmlsec.template.add_reference(
        signature, digest_algorithm_transform, uri=elem_id,
    )
    xmlsec.template.add_transform(ref, xmlsec.Transform.ENVELOPED)
    xmlsec.template.add_transform(ref, xmlsec.Transform.EXCL_C14N)
    key_info = xmlsec.template.ensure_key_info(signature)
    xmlsec.template.add_x509_data(key_info)

    dsig_ctx = xmlsec.SignatureContext()
    sign_key = xmlsec.Key.from_file(key, xmlsec.KeyFormat.PEM, None)
    sign_key.load_cert_from_file(cert, xmlsec.KeyFormat.PEM)

    dsig_ctx.key = sign_key
    dsig_ctx.sign(signature)

    return tostring(elem).decode()
예제 #17
0
    def validate_xml(xml, schema, debug=False):
        """
        Validates a xml against a schema
        :param xml: The xml that will be validated
        :type: string|DomDocument
        :param schema: The schema
        :type: string
        :param debug: If debug is active, the parse-errors will be showed
        :type: bool
        :returns: Error code or the DomDocument of the xml
        :rtype: string
        """
        assert isinstance(xml, basestring) or isinstance(
            xml, Document) or isinstance(xml, etree._Element)
        assert isinstance(schema, basestring)

        if isinstance(xml, Document):
            xml = xml.toxml()
        elif isinstance(xml, etree._Element):
            xml = tostring(xml, encoding='unicode')

        # Switch to lxml for schema validation
        try:
            dom = fromstring(xml.encode('utf-8'), forbid_dtd=True)
        except Exception:
            return 'unloaded_xml'

        schema_file = join(dirname(__file__), 'schemas', schema)
        f_schema = open(schema_file, 'r')
        schema_doc = etree.parse(f_schema)
        f_schema.close()
        xmlschema = etree.XMLSchema(schema_doc)

        if not xmlschema.validate(dom):
            if debug:
                stderr.write('Errors validating the metadata')
                stderr.write(':\n\n')
                for error in xmlschema.error_log:
                    stderr.write('%s\n' % error.message)

            return 'invalid_xml'

        return parseString(tostring(dom, encoding='unicode').encode('utf-8'),
                           forbid_dtd=True)
예제 #18
0
    def validate_xml(xml, schema, debug=False):
        """
        Validates a xml against a schema
        :param xml: The xml that will be validated
        :type: string|DomDocument
        :param schema: The schema
        :type: string
        :param debug: If debug is active, the parse-errors will be showed
        :type: bool
        :returns: Error code or the DomDocument of the xml
        :rtype: string
        """
        assert isinstance(xml, basestring) or isinstance(xml, Document) or isinstance(xml, etree._Element)
        assert isinstance(schema, basestring)

        if isinstance(xml, Document):
            xml = xml.toxml()
        elif isinstance(xml, etree._Element):
            xml = tostring(xml, encoding='unicode')

        # Switch to lxml for schema validation
        try:
            dom = fromstring(xml.encode('utf-8'), forbid_dtd=True)
        except Exception:
            return 'unloaded_xml'

        schema_file = join(dirname(__file__), 'schemas', schema)
        f_schema = open(schema_file, 'r')
        schema_doc = etree.parse(f_schema)
        f_schema.close()
        xmlschema = etree.XMLSchema(schema_doc)

        if not xmlschema.validate(dom):
            if debug:
                stderr.write('Errors validating the metadata')
                stderr.write(':\n\n')
                for error in xmlschema.error_log:
                    stderr.write('%s\n' % error.message)

            return 'invalid_xml'

        return parseString(tostring(dom, encoding='unicode').encode('utf-8'), forbid_dtd=True)
예제 #19
0
파일: utils.py 프로젝트: seecr/python-saml
    def validate_xml(xml, schema, debug=False):
        # TS: added caching of schema's
        """
        Validates a xml against a schema
        :param xml: The xml that will be validated
        :type: string|DomDocument
        :param schema: The schema
        :type: string
        :param debug: If debug is active, the parse-errors will be showed
        :type: bool
        :returns: Error code or the DomDocument of the xml
        :rtype: string
        """
        assert isinstance(xml, basestring) or isinstance(
            xml, Document) or isinstance(xml, etree._Element)
        assert isinstance(schema, basestring)

        if isinstance(xml, Document):
            xml = xml.toxml()
        elif isinstance(xml, etree._Element):
            xml = tostring(xml, encoding='unicode')

        # Switch to lxml for schema validation
        try:
            dom = fromstring(xml.encode('utf-8'))
        except Exception:
            return 'unloaded_xml'

        xmlschema = loadXmlSchemaByFilename(schema)

        if not xmlschema.validate(dom):
            if debug:
                sys.stderr.write('Errors validating the metadata')
                sys.stderr.write(':\n\n')
                for error in xmlschema.error_log:
                    sys.stderr.write('%s\n' % error.message)

                sys.stderr.flush()
            return 'invalid_xml'

        return parseString(tostring(dom, encoding='unicode').encode('utf-8'))
예제 #20
0
 def get_last_response_xml(self, pretty_print_if_possible=False):
     """
     Retrieves the raw XML (decrypted) of the last SAML response,
     or the last Logout Response generated or processed
     :returns: SAML response XML
     :rtype: string|None
     """
     response = None
     if self.__last_response is not None:
         if isinstance(self.__last_response, compat.str_type):
             response = self.__last_response
         else:
             response = tostring(self.__last_response, encoding='unicode', pretty_print=pretty_print_if_possible)
     return response
예제 #21
0
 def get_last_response_xml(self, pretty_print_if_possible=False):
     """
     Retrieves the raw XML (decrypted) of the last SAML response,
     or the last Logout Response generated or processed
     :returns: SAML response XML
     :rtype: string|None
     """
     response = None
     if self.__last_response is not None:
         if isinstance(self.__last_response, compat.str_type):
             response = self.__last_response
         else:
             response = tostring(self.__last_response, encoding='unicode', pretty_print=pretty_print_if_possible)
     return response
예제 #22
0
def createSigniature(student, key):
    root = ElementTree.Element("Student")
    username = ElementTree.SubElement(root, "Username")
    username.text = student["username"]
    fakulteti = ElementTree.SubElement(root, "Fakulteti")
    fakulteti.text = student["fakulteti"]
    nota = ElementTree.SubElement(root, "Nota")
    nota.text = student["nota"]
    viti = ElementTree.SubElement(root, "Viti")
    viti.text = student["viti"]
    signedXML = XMLSigner().sign(root, key=PRIVATEKEY)
    xmlStr = tostring(signedXML)
    message = {"cmd": "SIGNIATURE", "data": xmlStr.decode()}
    message = json.dumps(message)
    return MessageToSend(key, message)
예제 #23
0
    def generate_metadata_xml(self):
        """Helper method to generate the SP metadata XML. Extends the base class
        metadata generation with extensions, elements and attributes required by
        the Suomi.fi IdP.

        Returns (metadata XML string, list of errors)
        """
        metadata, errors = super().generate_metadata_xml()

        metadata_xml = fromstring(metadata)
        self._amend_services(metadata_xml)
        self._amend_contacts(metadata_xml)
        self._add_entity_attributes_extension(metadata_xml)
        self._add_ui_info_extension(metadata_xml)
        metadata = tostring(metadata_xml, encoding='utf-8', xml_declaration=True)

        return metadata, errors
예제 #24
0
def node_to_string(node):
    """

    Parameters
    ----------
    node : lxml note

    Returns
    -------

    str :
    Pretty string representation of node
    """
    return lxml.tostring(node,
                         pretty_print=True,
                         with_tail=False,
                         encoding='UTF-8',
                         xml_declaration=True).decode("utf-8")
예제 #25
0
def xpath_selector(selector, html, select_all):
    """
    Returns Xpath match for `selector` within `html`.

    :param selector: XPath string
    :param html: Unicode content
    :param select_all: True to get all matches
    """
    from defusedxml import lxml as dlxml
    from lxml import etree
    import re

    # lxml requires argument to be bytes
    # see https://github.com/kibitzr/kibitzr/issues/47
    encoded = html.encode('utf-8')
    root = dlxml.fromstring(encoded, parser=etree.HTMLParser())
    xpath_results = root.xpath(selector)
    if not xpath_results:
        logger.warning('XPath selector not found: %r', selector)
        return False, html

    if isinstance(xpath_results, list):
        if select_all is False:
            xpath_results = xpath_results[0:1]
    else:
        xpath_results = [xpath_results]

    # Serialize xpath_results
    # see https://lxml.de/xpathxslt.html#xpath-return-values
    results = []
    for r in xpath_results:
        # namespace declarations
        if isinstance(r, tuple):
            results.append("%s=\"%s\"" % (r[0], r[1]))
        # an element
        elif hasattr(r, 'tag'):
            results.append(
                re.sub(r'\s+', ' ',
                       dlxml.tostring(r, method='html', encoding='unicode')))
        else:
            results.append(r)

    return True, u"\n".join(six.text_type(x).strip() for x in results)
예제 #26
0
    def generate_metadata_xml(self):
        """Helper method to generate the SP metadata XML. Extends the base class
        metadata generation with extensions, elements and attributes required by
        the Suomi.fi IdP.

        Returns (metadata XML string, list of errors)
        """
        metadata, errors = super().generate_metadata_xml()

        metadata_xml = fromstring(metadata)
        self._amend_services(metadata_xml)
        self._amend_contacts(metadata_xml)
        self._add_entity_attributes_extension(metadata_xml)
        self._add_ui_info_extension(metadata_xml)
        metadata = tostring(metadata_xml,
                            encoding='utf-8',
                            xml_declaration=True)

        return metadata, errors
예제 #27
0
    def validate_xml(xml, schema, debug=False):
        """
        Validates a xml against a schema
        :param xml: The xml that will be validated
        :type: string|DomDocument
        :param schema: The schema
        :type: string
        :param debug: If debug is active, the parse-errors will be showed
        :type: bool
        :returns: Error code or the DomDocument of the xml
        :rtype: string
        """
        assert isinstance(xml, basestring) or isinstance(xml, Document) or isinstance(xml, etree._Element)
        assert isinstance(schema, basestring)

        if isinstance(xml, Document):
            xml = xml.toxml()
        elif isinstance(xml, etree._Element):
            xml = tostring(xml)

        # Switch to lxml for schema validation
        try:
            dom = fromstring(str(xml))
        except Exception:
            return "unloaded_xml"

        schema_file = join(dirname(__file__), "schemas", schema)
        f_schema = open(schema_file, "r")
        schema_doc = etree.parse(f_schema)
        f_schema.close()
        xmlschema = etree.XMLSchema(schema_doc)

        if not xmlschema.validate(dom):
            if debug:
                stderr.write("Errors validating the metadata")
                stderr.write(":\n\n")
                for error in xmlschema.error_log:
                    stderr.write("%s\n" % error.message)

            return "invalid_xml"

        return parseString(etree.tostring(dom))
예제 #28
0
    def is_valid(self, request_data, request_id=None, raise_exceptions=False):
        """
        Validates the response object.

        :param request_data: Request Data
        :type request_data: dict

        :param request_id: Optional argument. The ID of the AuthNRequest sent by this SP to the IdP
        :type request_id: string

        :param raise_exceptions: Whether to return false on failure or raise an exception
        :type raise_exceptions: Boolean

        :returns: True if the SAML Response is valid, False if not
        :rtype: bool
        """
        self.__error = None
        try:
            # Checks SAML version
            if self.document.get('Version', None) != '2.0':
                raise OneLogin_Saml2_ValidationError(
                    'Unsupported SAML version',
                    OneLogin_Saml2_ValidationError.UNSUPPORTED_SAML_VERSION)

            # Checks that ID exists
            if self.document.get('ID', None) is None:
                raise OneLogin_Saml2_ValidationError(
                    'Missing ID attribute on SAML Response',
                    OneLogin_Saml2_ValidationError.MISSING_ID)

            # Checks that the response has the SUCCESS status
            self.check_status()

            # Checks that the response only has one assertion
            if not self.validate_num_assertions():
                raise OneLogin_Saml2_ValidationError(
                    'SAML Response must contain 1 assertion',
                    OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_ASSERTIONS)

            idp_data = self.__settings.get_idp_data()
            idp_entity_id = idp_data.get('entityId', '')
            sp_data = self.__settings.get_sp_data()
            sp_entity_id = sp_data.get('entityId', '')

            signed_elements = self.process_signed_elements()

            has_signed_response = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements
            has_signed_assertion = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML in signed_elements

            if self.__settings.is_strict():
                no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd'
                res = OneLogin_Saml2_Utils.validate_xml(
                    tostring(self.document), 'saml-schema-protocol-2.0.xsd',
                    self.__settings.is_debug_active())
                if not isinstance(res, Document):
                    raise OneLogin_Saml2_ValidationError(
                        no_valid_xml_msg,
                        OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT)

                # If encrypted, check also the decrypted document
                if self.encrypted:
                    res = OneLogin_Saml2_Utils.validate_xml(
                        tostring(self.decrypted_document),
                        'saml-schema-protocol-2.0.xsd',
                        self.__settings.is_debug_active())
                    if not isinstance(res, Document):
                        raise OneLogin_Saml2_ValidationError(
                            no_valid_xml_msg,
                            OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT)

                security = self.__settings.get_security_data()
                current_url = OneLogin_Saml2_Utils.get_self_url_no_query(
                    request_data)

                in_response_to = self.document.get('InResponseTo', None)
                if request_id is None and in_response_to is not None and security.get(
                        'rejectUnsolicitedResponsesWithInResponseTo', False):
                    raise OneLogin_Saml2_ValidationError(
                        'The Response has an InResponseTo attribute: %s while no InResponseTo was expected'
                        % in_response_to,
                        OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO)

                # Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided
                if request_id is not None and in_response_to != request_id:
                    raise OneLogin_Saml2_ValidationError(
                        'The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s'
                        % (in_response_to, request_id),
                        OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO)

                if not self.encrypted and security.get(
                        'wantAssertionsEncrypted', False):
                    raise OneLogin_Saml2_ValidationError(
                        'The assertion of the Response is not encrypted and the SP require it',
                        OneLogin_Saml2_ValidationError.NO_ENCRYPTED_ASSERTION)

                if security.get('wantNameIdEncrypted', False):
                    encrypted_nameid_nodes = self.__query_assertion(
                        '/saml:Subject/saml:EncryptedID/xenc:EncryptedData')
                    if len(encrypted_nameid_nodes) != 1:
                        raise OneLogin_Saml2_ValidationError(
                            'The NameID of the Response is not encrypted and the SP require it',
                            OneLogin_Saml2_ValidationError.NO_ENCRYPTED_NAMEID)

                # Checks that a Conditions element exists
                if not self.check_one_condition():
                    raise OneLogin_Saml2_ValidationError(
                        'The Assertion must include a Conditions element',
                        OneLogin_Saml2_ValidationError.MISSING_CONDITIONS)

                # Validates Assertion timestamps
                self.validate_timestamps(raise_exceptions=True)

                # Checks that an AuthnStatement element exists and is unique
                if not self.check_one_authnstatement():
                    raise OneLogin_Saml2_ValidationError(
                        'The Assertion must include an AuthnStatement element',
                        OneLogin_Saml2_ValidationError.
                        WRONG_NUMBER_OF_AUTHSTATEMENTS)

                # Checks that there is at least one AttributeStatement if required
                attribute_statement_nodes = self.__query_assertion(
                    '/saml:AttributeStatement')
                if security.get('wantAttributeStatement',
                                True) and not attribute_statement_nodes:
                    raise OneLogin_Saml2_ValidationError(
                        'There is no AttributeStatement on the Response',
                        OneLogin_Saml2_ValidationError.NO_ATTRIBUTESTATEMENT)

                encrypted_attributes_nodes = self.__query_assertion(
                    '/saml:AttributeStatement/saml:EncryptedAttribute')
                if encrypted_attributes_nodes:
                    raise OneLogin_Saml2_ValidationError(
                        'There is an EncryptedAttribute in the Response and this SP not support them',
                        OneLogin_Saml2_ValidationError.ENCRYPTED_ATTRIBUTES)

                # Checks destination
                destination = self.document.get('Destination', None)
                if destination:
                    if not destination.startswith(current_url):
                        # TODO: Review if following lines are required, since we can control the
                        # request_data
                        #  current_url_routed = OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data)
                        #  if not destination.startswith(current_url_routed):
                        raise OneLogin_Saml2_ValidationError(
                            'The response was received at %s instead of %s' %
                            (current_url, destination),
                            OneLogin_Saml2_ValidationError.WRONG_DESTINATION)
                elif destination == '':
                    raise OneLogin_Saml2_ValidationError(
                        'The response has an empty Destination value',
                        OneLogin_Saml2_ValidationError.EMPTY_DESTINATION)

                # Checks audience
                valid_audiences = self.get_audiences()
                if valid_audiences and sp_entity_id not in valid_audiences:
                    raise OneLogin_Saml2_ValidationError(
                        '%s is not a valid audience for this Response' %
                        sp_entity_id,
                        OneLogin_Saml2_ValidationError.WRONG_AUDIENCE)

                # Checks the issuers
                issuers = self.get_issuers()
                for issuer in issuers:
                    if issuer is None or issuer != idp_entity_id:
                        raise OneLogin_Saml2_ValidationError(
                            'Invalid issuer in the Assertion/Response (expected %(idpEntityId)s, got %(issuer)s)'
                            % {
                                'idpEntityId': idp_entity_id,
                                'issuer': issuer
                            }, OneLogin_Saml2_ValidationError.WRONG_ISSUER)

                # Checks the session Expiration
                session_expiration = self.get_session_not_on_or_after()
                if session_expiration and session_expiration <= OneLogin_Saml2_Utils.now(
                ):
                    raise OneLogin_Saml2_ValidationError(
                        'The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response',
                        OneLogin_Saml2_ValidationError.SESSION_EXPIRED)

                # Checks the SubjectConfirmation, at least one SubjectConfirmation must be valid
                any_subject_confirmation = False
                subject_confirmation_nodes = self.__query_assertion(
                    '/saml:Subject/saml:SubjectConfirmation')

                for scn in subject_confirmation_nodes:
                    method = scn.get('Method', None)
                    if method and method != OneLogin_Saml2_Constants.CM_BEARER:
                        continue
                    sc_data = scn.find(
                        'saml:SubjectConfirmationData',
                        namespaces=OneLogin_Saml2_Constants.NSMAP)
                    if sc_data is None:
                        continue
                    else:
                        irt = sc_data.get('InResponseTo', None)
                        if (in_response_to is None and irt is not None and
                           security.get('rejectUnsolicitedResponsesWithInResponseTo', False)) or \
                           in_response_to and irt and irt != in_response_to:
                            continue
                        recipient = sc_data.get('Recipient', None)
                        if recipient and current_url not in recipient:
                            continue
                        nooa = sc_data.get('NotOnOrAfter', None)
                        if nooa:
                            parsed_nooa = OneLogin_Saml2_Utils.parse_SAML_to_time(
                                nooa)
                            if parsed_nooa <= OneLogin_Saml2_Utils.now():
                                continue
                        nb = sc_data.get('NotBefore', None)
                        if nb:
                            parsed_nb = OneLogin_Saml2_Utils.parse_SAML_to_time(
                                nb)
                            if parsed_nb > OneLogin_Saml2_Utils.now():
                                continue

                        if nooa:
                            self.valid_scd_not_on_or_after = OneLogin_Saml2_Utils.parse_SAML_to_time(
                                nooa)

                        any_subject_confirmation = True
                        break

                if not any_subject_confirmation:
                    raise OneLogin_Saml2_ValidationError(
                        'A valid SubjectConfirmation was not found on this Response',
                        OneLogin_Saml2_ValidationError.
                        WRONG_SUBJECTCONFIRMATION)

                if security.get('wantAssertionsSigned',
                                False) and not has_signed_assertion:
                    raise OneLogin_Saml2_ValidationError(
                        'The Assertion of the Response is not signed and the SP require it',
                        OneLogin_Saml2_ValidationError.NO_SIGNED_ASSERTION)

                if security.get('wantMessagesSigned',
                                False) and not has_signed_response:
                    raise OneLogin_Saml2_ValidationError(
                        'The Message of the Response is not signed and the SP require it',
                        OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE)

            if not signed_elements or (not has_signed_response
                                       and not has_signed_assertion):
                raise OneLogin_Saml2_ValidationError(
                    'No Signature found. SAML Response rejected',
                    OneLogin_Saml2_ValidationError.NO_SIGNATURE_FOUND)
            else:
                cert = idp_data.get('x509cert', None)
                fingerprint = idp_data.get('certFingerprint', None)
                fingerprintalg = idp_data.get('certFingerprintAlgorithm', None)

                multicerts = None
                if 'x509certMulti' in idp_data and 'signing' in idp_data[
                        'x509certMulti'] and idp_data['x509certMulti'][
                            'signing']:
                    multicerts = idp_data['x509certMulti']['signing']

                # If find a Signature on the Response, validates it checking the original response
                if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(
                        self.document,
                        cert,
                        fingerprint,
                        fingerprintalg,
                        xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH,
                        multicerts=multicerts,
                        raise_exceptions=False):
                    raise OneLogin_Saml2_ValidationError(
                        'Signature validation failed. SAML Response rejected',
                        OneLogin_Saml2_ValidationError.INVALID_SIGNATURE)

                document_check_assertion = self.decrypted_document if self.encrypted else self.document
                if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(
                        document_check_assertion,
                        cert,
                        fingerprint,
                        fingerprintalg,
                        xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH,
                        multicerts=multicerts,
                        raise_exceptions=False):
                    raise OneLogin_Saml2_ValidationError(
                        'Signature validation failed. SAML Response rejected',
                        OneLogin_Saml2_ValidationError.INVALID_SIGNATURE)

            return True
        except Exception as err:
            self.__error = err.__str__()
            debug = self.__settings.is_debug_active()
            if debug:
                print err.__str__()
            if raise_exceptions:
                raise err
            return False
예제 #29
0
    def add_sign(xml,
                 key,
                 cert,
                 debug=False,
                 sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1,
                 digest_algorithm=OneLogin_Saml2_Constants.SHA1):
        """
        Adds signature key and senders certificate to an element (Message or
        Assertion).

        :param xml: The element we should sign
        :type: string | Document

        :param key: The private key
        :type: string

        :param cert: The public
        :type: string

        :param debug: Activate the xmlsec debug
        :type: bool

        :param sign_algorithm: Signature algorithm method
        :type sign_algorithm: string

        :param digest_algorithm: Digest algorithm method
        :type digest_algorithm: string

        :returns: Signed XML
        :rtype: string
        """
        if xml is None or xml == '':
            raise Exception('Empty string supplied as input')
        elif isinstance(xml, etree._Element):
            elem = xml
        elif isinstance(xml, Document):
            xml = xml.toxml()
            elem = fromstring(xml.encode('utf-8'), forbid_dtd=True)
        elif isinstance(xml, Element):
            xml.setAttributeNS(unicode(OneLogin_Saml2_Constants.NS_SAMLP),
                               'xmlns:samlp',
                               unicode(OneLogin_Saml2_Constants.NS_SAMLP))
            xml.setAttributeNS(unicode(OneLogin_Saml2_Constants.NS_SAML),
                               'xmlns:saml',
                               unicode(OneLogin_Saml2_Constants.NS_SAML))
            xml = xml.toxml()
            elem = fromstring(xml.encode('utf-8'), forbid_dtd=True)
        elif isinstance(xml, basestring):
            elem = fromstring(xml.encode('utf-8'), forbid_dtd=True)
        else:
            raise Exception('Error parsing xml string')

        error_callback_method = None
        if debug:
            error_callback_method = print_xmlsec_errors
        xmlsec.set_error_callback(error_callback_method)

        sign_algorithm_transform_map = {
            OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1,
            OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1,
            OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256,
            OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384,
            OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512
        }
        sign_algorithm_transform = sign_algorithm_transform_map.get(
            sign_algorithm, xmlsec.TransformRsaSha1)

        signature = Signature(xmlsec.TransformExclC14N,
                              sign_algorithm_transform,
                              nsPrefix='ds')

        issuer = OneLogin_Saml2_Utils.query(elem, '//saml:Issuer')
        if len(issuer) > 0:
            issuer = issuer[0]
            issuer.addnext(signature)
            elem_to_sign = issuer.getparent()
        else:
            entity_descriptor = OneLogin_Saml2_Utils.query(
                elem, '//md:EntityDescriptor')
            if len(entity_descriptor) > 0:
                elem.insert(0, signature)
            else:
                elem[0].insert(0, signature)
            elem_to_sign = elem

        elem_id = elem_to_sign.get('ID', None)
        if elem_id is not None:
            if elem_id:
                elem_id = '#' + elem_id
        else:
            generated_id = generated_id = OneLogin_Saml2_Utils.generate_unique_id(
            )
            elem_id = '#' + generated_id
            elem_to_sign.attrib['ID'] = generated_id

        xmlsec.addIDs(elem_to_sign, ["ID"])

        digest_algorithm_transform_map = {
            OneLogin_Saml2_Constants.SHA1: xmlsec.TransformSha1,
            OneLogin_Saml2_Constants.SHA256: xmlsec.TransformSha256,
            OneLogin_Saml2_Constants.SHA384: xmlsec.TransformSha384,
            OneLogin_Saml2_Constants.SHA512: xmlsec.TransformSha512
        }
        digest_algorithm_transform = digest_algorithm_transform_map.get(
            digest_algorithm, xmlsec.TransformSha1)

        ref = signature.addReference(digest_algorithm_transform)
        if elem_id:
            ref.attrib['URI'] = elem_id

        ref.addTransform(xmlsec.TransformEnveloped)
        ref.addTransform(xmlsec.TransformExclC14N)

        key_info = signature.ensureKeyInfo()
        key_info.addX509Data()

        dsig_ctx = xmlsec.DSigCtx()
        sign_key = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None)

        file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
        sign_key.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem)
        file_cert.close()

        dsig_ctx.signKey = sign_key
        dsig_ctx.sign(signature)

        return tostring(elem, encoding='unicode').encode('utf-8')
예제 #30
0
    def generate_name_id(value,
                         sp_nq,
                         sp_format=None,
                         cert=None,
                         debug=False,
                         nq=None):
        """
        Generates a nameID.

        :param value: fingerprint
        :type: string

        :param sp_nq: SP Name Qualifier
        :type: string

        :param sp_format: SP Format
        :type: string

        :param cert: IdP Public Cert to encrypt the nameID
        :type: string

        :param debug: Activate the xmlsec debug
        :type: bool

        :param nq: IDP Name Qualifier
        :type: string

        :returns: DOMElement | XMLSec nameID
        :rtype: string
        """
        doc = Document()
        name_id_container = doc.createElementNS(
            OneLogin_Saml2_Constants.NS_SAML, 'container')
        name_id_container.setAttribute("xmlns:saml",
                                       OneLogin_Saml2_Constants.NS_SAML)

        name_id = doc.createElement('saml:NameID')
        if sp_nq is not None:
            name_id.setAttribute('SPNameQualifier', sp_nq)
        if nq is not None:
            name_id.setAttribute('NameQualifier', nq)
        if sp_format is not None:
            name_id.setAttribute('Format', sp_format)
        name_id.appendChild(doc.createTextNode(value))
        name_id_container.appendChild(name_id)

        if cert is not None:
            xml = name_id_container.toxml()
            elem = fromstring(xml)

            error_callback_method = None
            if debug:
                error_callback_method = print_xmlsec_errors
            xmlsec.set_error_callback(error_callback_method)

            # Load the public cert
            mngr = xmlsec.KeysMngr()
            file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
            key_data = xmlsec.Key.load(file_cert.name,
                                       xmlsec.KeyDataFormatCertPem, None)
            key_data.name = basename(file_cert.name)
            mngr.addKey(key_data)
            file_cert.close()

            # Prepare for encryption
            enc_data = EncData(xmlsec.TransformAes128Cbc,
                               type=xmlsec.TypeEncElement)
            enc_data.ensureCipherValue()
            key_info = enc_data.ensureKeyInfo()
            # enc_key = key_info.addEncryptedKey(xmlsec.TransformRsaPkcs1)
            enc_key = key_info.addEncryptedKey(xmlsec.TransformRsaOaep)
            enc_key.ensureCipherValue()

            # Encrypt!
            enc_ctx = xmlsec.EncCtx(mngr)
            enc_ctx.encKey = xmlsec.Key.generate(xmlsec.KeyDataAes, 128,
                                                 xmlsec.KeyDataTypeSession)

            edata = enc_ctx.encryptXml(enc_data, elem[0])

            newdoc = parseString(
                tostring(edata, encoding='unicode').encode('utf-8'))

            if newdoc.hasChildNodes():
                child = newdoc.firstChild
                child.removeAttribute('xmlns')
                child.removeAttribute('xmlns:saml')
                child.setAttribute('xmlns:xenc',
                                   OneLogin_Saml2_Constants.NS_XENC)
                child.setAttribute('xmlns:dsig',
                                   OneLogin_Saml2_Constants.NS_DS)

            nodes = newdoc.getElementsByTagName("*")
            for node in nodes:
                if node.tagName == 'ns0:KeyInfo':
                    node.tagName = 'dsig:KeyInfo'
                    node.removeAttribute('xmlns:ns0')
                    node.setAttribute('xmlns:dsig',
                                      OneLogin_Saml2_Constants.NS_DS)
                else:
                    node.tagName = 'xenc:' + node.tagName

            encrypted_id = newdoc.createElement('saml:EncryptedID')
            encrypted_data = newdoc.replaceChild(encrypted_id,
                                                 newdoc.firstChild)
            encrypted_id.appendChild(encrypted_data)
            return newdoc.saveXML(encrypted_id)
        else:
            return doc.saveXML(name_id)
예제 #31
0
    def is_valid(self, request_data, request_id=None, raise_exceptions=False):
        """
        Validates the response object.

        :param request_data: Request Data
        :type request_data: dict

        :param request_id: Optional argument. The ID of the AuthNRequest sent by this SP to the IdP
        :type request_id: string

        :param raise_exceptions: Whether to return false on failure or raise an exception
        :type raise_exceptions: Boolean

        :returns: True if the SAML Response is valid, False if not
        :rtype: bool
        """
        self.__error = None
        try:
            # Checks SAML version
            if self.document.get('Version', None) != '2.0':
                raise OneLogin_Saml2_ValidationError(
                    'Unsupported SAML version',
                    OneLogin_Saml2_ValidationError.UNSUPPORTED_SAML_VERSION
                )

            # Checks that ID exists
            if self.document.get('ID', None) is None:
                raise OneLogin_Saml2_ValidationError(
                    'Missing ID attribute on SAML Response',
                    OneLogin_Saml2_ValidationError.MISSING_ID
                )

            # Checks that the response has the SUCCESS status
            self.check_status()

            # Checks that the response only has one assertion
            if not self.validate_num_assertions():
                raise OneLogin_Saml2_ValidationError(
                    'SAML Response must contain 1 assertion',
                    OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_ASSERTIONS
                )

            idp_data = self.__settings.get_idp_data()
            idp_entity_id = idp_data.get('entityId', '')
            sp_data = self.__settings.get_sp_data()
            sp_entity_id = sp_data.get('entityId', '')

            signed_elements = self.process_signed_elements()

            has_signed_response = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements
            has_signed_assertion = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML in signed_elements

            if self.__settings.is_strict():
                no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd'
                res = OneLogin_Saml2_Utils.validate_xml(
                    tostring(self.document),
                    'saml-schema-protocol-2.0.xsd',
                    self.__settings.is_debug_active()
                )
                if not isinstance(res, Document):
                    raise OneLogin_Saml2_ValidationError(
                        no_valid_xml_msg,
                        OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT
                    )

                # If encrypted, check also the decrypted document
                if self.encrypted:
                    res = OneLogin_Saml2_Utils.validate_xml(
                        tostring(self.decrypted_document),
                        'saml-schema-protocol-2.0.xsd',
                        self.__settings.is_debug_active()
                    )
                    if not isinstance(res, Document):
                        raise OneLogin_Saml2_ValidationError(
                            no_valid_xml_msg,
                            OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT
                        )

                security = self.__settings.get_security_data()
                current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)

                in_response_to = self.document.get('InResponseTo', None)
                if request_id is None and in_response_to is not None and security.get('rejectUnsolicitedResponsesWithInResponseTo', False):
                    raise OneLogin_Saml2_ValidationError(
                        'The Response has an InResponseTo attribute: %s while no InResponseTo was expected' % in_response_to,
                        OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
                    )

                # Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided
                if request_id is not None and in_response_to != request_id:
                    raise OneLogin_Saml2_ValidationError(
                        'The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id),
                        OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
                    )

                if not self.encrypted and security.get('wantAssertionsEncrypted', False):
                    raise OneLogin_Saml2_ValidationError(
                        'The assertion of the Response is not encrypted and the SP require it',
                        OneLogin_Saml2_ValidationError.NO_ENCRYPTED_ASSERTION
                    )

                if security.get('wantNameIdEncrypted', False):
                    encrypted_nameid_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData')
                    if len(encrypted_nameid_nodes) != 1:
                        raise OneLogin_Saml2_ValidationError(
                            'The NameID of the Response is not encrypted and the SP require it',
                            OneLogin_Saml2_ValidationError.NO_ENCRYPTED_NAMEID
                        )

                # Checks that a Conditions element exists
                if not self.check_one_condition():
                    raise OneLogin_Saml2_ValidationError(
                        'The Assertion must include a Conditions element',
                        OneLogin_Saml2_ValidationError.MISSING_CONDITIONS
                    )

                # Validates Assertion timestamps
                self.validate_timestamps(raise_exceptions=True)

                # Checks that an AuthnStatement element exists and is unique
                if not self.check_one_authnstatement():
                    raise OneLogin_Saml2_ValidationError(
                        'The Assertion must include an AuthnStatement element',
                        OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_AUTHSTATEMENTS
                    )

                # Checks that the response has all of the AuthnContexts that we provided in the request.
                # Only check if failOnAuthnContextMismatch is true and requestedAuthnContext is set to a list.
                requested_authn_contexts = security.get('requestedAuthnContext', True)

                if security.get('failOnAuthnContextMismatch', False) and requested_authn_contexts and requested_authn_contexts is not True:
                    authn_contexts = self.get_authn_contexts()
                    unmatched_contexts = set(requested_authn_contexts).difference(authn_contexts)
                    if unmatched_contexts:
                        raise OneLogin_Saml2_ValidationError(
                            'The AuthnContext "%s" didn\'t include requested context "%s"' % (', '.join(authn_contexts), ', '.join(unmatched_contexts)),
                            OneLogin_Saml2_ValidationError.AUTHN_CONTEXT_MISMATCH
                        )

                # Checks that there is at least one AttributeStatement if required
                attribute_statement_nodes = self.__query_assertion('/saml:AttributeStatement')
                if security.get('wantAttributeStatement', True) and not attribute_statement_nodes:
                    raise OneLogin_Saml2_ValidationError(
                        'There is no AttributeStatement on the Response',
                        OneLogin_Saml2_ValidationError.NO_ATTRIBUTESTATEMENT
                    )

                encrypted_attributes_nodes = self.__query_assertion('/saml:AttributeStatement/saml:EncryptedAttribute')
                if encrypted_attributes_nodes:
                    raise OneLogin_Saml2_ValidationError(
                        'There is an EncryptedAttribute in the Response and this SP not support them',
                        OneLogin_Saml2_ValidationError.ENCRYPTED_ATTRIBUTES
                    )

                # Checks destination
                destination = self.document.get('Destination', None)
                if destination:
                    if not destination.startswith(current_url):
                        # TODO: Review if following lines are required, since we can control the
                        # request_data
                        #  current_url_routed = OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data)
                        #  if not destination.startswith(current_url_routed):
                        raise OneLogin_Saml2_ValidationError(
                            'The response was received at %s instead of %s' % (current_url, destination),
                            OneLogin_Saml2_ValidationError.WRONG_DESTINATION
                        )
                elif destination == '':
                    raise OneLogin_Saml2_ValidationError(
                        'The response has an empty Destination value',
                        OneLogin_Saml2_ValidationError.EMPTY_DESTINATION
                    )

                # Checks audience
                valid_audiences = self.get_audiences()
                if valid_audiences and sp_entity_id not in valid_audiences:
                    raise OneLogin_Saml2_ValidationError(
                        '%s is not a valid audience for this Response' % sp_entity_id,
                        OneLogin_Saml2_ValidationError.WRONG_AUDIENCE
                    )

                # Checks the issuers
                issuers = self.get_issuers()
                for issuer in issuers:
                    if issuer is None or issuer != idp_entity_id:
                        raise OneLogin_Saml2_ValidationError(
                            'Invalid issuer in the Assertion/Response (expected %(idpEntityId)s, got %(issuer)s)' %
                            {
                                'idpEntityId': idp_entity_id,
                                'issuer': issuer
                            },
                            OneLogin_Saml2_ValidationError.WRONG_ISSUER
                        )

                # Checks the session Expiration
                session_expiration = self.get_session_not_on_or_after()
                if session_expiration and session_expiration <= OneLogin_Saml2_Utils.now():
                    raise OneLogin_Saml2_ValidationError(
                        'The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response',
                        OneLogin_Saml2_ValidationError.SESSION_EXPIRED
                    )

                # Checks the SubjectConfirmation, at least one SubjectConfirmation must be valid
                any_subject_confirmation = False
                subject_confirmation_nodes = self.__query_assertion('/saml:Subject/saml:SubjectConfirmation')

                for scn in subject_confirmation_nodes:
                    method = scn.get('Method', None)
                    if method and method != OneLogin_Saml2_Constants.CM_BEARER:
                        continue
                    sc_data = scn.find('saml:SubjectConfirmationData', namespaces=OneLogin_Saml2_Constants.NSMAP)
                    if sc_data is None:
                        continue
                    else:
                        irt = sc_data.get('InResponseTo', None)
                        if (in_response_to is None and irt is not None and
                           security.get('rejectUnsolicitedResponsesWithInResponseTo', False)) or \
                           in_response_to and irt and irt != in_response_to:
                            continue
                        recipient = sc_data.get('Recipient', None)
                        if recipient and current_url not in recipient:
                            continue
                        nooa = sc_data.get('NotOnOrAfter', None)
                        if nooa:
                            parsed_nooa = OneLogin_Saml2_Utils.parse_SAML_to_time(nooa)
                            if parsed_nooa <= OneLogin_Saml2_Utils.now():
                                continue
                        nb = sc_data.get('NotBefore', None)
                        if nb:
                            parsed_nb = OneLogin_Saml2_Utils.parse_SAML_to_time(nb)
                            if parsed_nb > OneLogin_Saml2_Utils.now():
                                continue

                        if nooa:
                            self.valid_scd_not_on_or_after = OneLogin_Saml2_Utils.parse_SAML_to_time(nooa)

                        any_subject_confirmation = True
                        break

                if not any_subject_confirmation:
                    raise OneLogin_Saml2_ValidationError(
                        'A valid SubjectConfirmation was not found on this Response',
                        OneLogin_Saml2_ValidationError.WRONG_SUBJECTCONFIRMATION
                    )

                if security.get('wantAssertionsSigned', False) and not has_signed_assertion:
                    raise OneLogin_Saml2_ValidationError(
                        'The Assertion of the Response is not signed and the SP require it',
                        OneLogin_Saml2_ValidationError.NO_SIGNED_ASSERTION
                    )

                if security.get('wantMessagesSigned', False) and not has_signed_response:
                    raise OneLogin_Saml2_ValidationError(
                        'The Message of the Response is not signed and the SP require it',
                        OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE
                    )

            if not signed_elements or (not has_signed_response and not has_signed_assertion):
                raise OneLogin_Saml2_ValidationError(
                    'No Signature found. SAML Response rejected',
                    OneLogin_Saml2_ValidationError.NO_SIGNATURE_FOUND
                )
            else:
                cert = idp_data.get('x509cert', None)
                fingerprint = idp_data.get('certFingerprint', None)
                fingerprintalg = idp_data.get('certFingerprintAlgorithm', None)

                multicerts = None
                if 'x509certMulti' in idp_data and 'signing' in idp_data['x509certMulti'] and idp_data['x509certMulti']['signing']:
                    multicerts = idp_data['x509certMulti']['signing']

                # If find a Signature on the Response, validates it checking the original response
                if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, multicerts=multicerts, raise_exceptions=False):
                    raise OneLogin_Saml2_ValidationError(
                        'Signature validation failed. SAML Response rejected',
                        OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
                    )

                document_check_assertion = self.decrypted_document if self.encrypted else self.document
                if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, multicerts=multicerts, raise_exceptions=False):
                    raise OneLogin_Saml2_ValidationError(
                        'Signature validation failed. SAML Response rejected',
                        OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
                    )

            return True
        except Exception as err:
            self.__error = err.__str__()
            debug = self.__settings.is_debug_active()
            if debug:
                print(err.__str__())
            if raise_exceptions:
                raise err
            return False
예제 #32
0
 def encode(self):
     return lxml.tostring(
         self.to_element(ElementMaker(nsmap=self.nsmap)),
         xml_declaration=True,
         encoding='UTF-8',
     )
예제 #33
0
    def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1, digest_algorithm=OneLogin_Saml2_Constants.SHA1):
        """
        Adds signature key and senders certificate to an element (Message or
        Assertion).

        :param xml: The element we should sign
        :type: string | Document

        :param key: The private key
        :type: string

        :param cert: The public
        :type: string

        :param debug: Activate the xmlsec debug
        :type: bool

        :param sign_algorithm: Signature algorithm method
        :type sign_algorithm: string

        :param digest_algorithm: Digest algorithm method
        :type digest_algorithm: string

        :returns: Signed XML
        :rtype: string
        """
        if xml is None or xml == '':
            raise Exception('Empty string supplied as input')
        elif isinstance(xml, etree._Element):
            elem = xml
        elif isinstance(xml, Document):
            xml = xml.toxml()
            elem = fromstring(xml.encode('utf-8'), forbid_dtd=True)
        elif isinstance(xml, Element):
            xml.setAttributeNS(
                unicode(OneLogin_Saml2_Constants.NS_SAMLP),
                'xmlns:samlp',
                unicode(OneLogin_Saml2_Constants.NS_SAMLP)
            )
            xml.setAttributeNS(
                unicode(OneLogin_Saml2_Constants.NS_SAML),
                'xmlns:saml',
                unicode(OneLogin_Saml2_Constants.NS_SAML)
            )
            xml = xml.toxml()
            elem = fromstring(xml.encode('utf-8'), forbid_dtd=True)
        elif isinstance(xml, basestring):
            elem = fromstring(xml.encode('utf-8'), forbid_dtd=True)
        else:
            raise Exception('Error parsing xml string')

        error_callback_method = None
        if debug:
            error_callback_method = print_xmlsec_errors
        xmlsec.set_error_callback(error_callback_method)

        sign_algorithm_transform_map = {
            OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1,
            OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1,
            OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256,
            OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384,
            OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512
        }
        sign_algorithm_transform = sign_algorithm_transform_map.get(sign_algorithm, xmlsec.TransformRsaSha1)

        signature = Signature(xmlsec.TransformExclC14N, sign_algorithm_transform, nsPrefix='ds')

        issuer = OneLogin_Saml2_Utils.query(elem, '//saml:Issuer')
        if len(issuer) > 0:
            issuer = issuer[0]
            issuer.addnext(signature)
            elem_to_sign = issuer.getparent()
        else:
            entity_descriptor = OneLogin_Saml2_Utils.query(elem, '//md:EntityDescriptor')
            if len(entity_descriptor) > 0:
                elem.insert(0, signature)
            else:
                elem[0].insert(0, signature)
            elem_to_sign = elem

        elem_id = elem_to_sign.get('ID', None)
        if elem_id is not None:
            if elem_id:
                elem_id = '#' + elem_id
        else:
            generated_id = generated_id = OneLogin_Saml2_Utils.generate_unique_id()
            elem_id = '#' + generated_id
            elem_to_sign.attrib['ID'] = generated_id

        xmlsec.addIDs(elem_to_sign, ["ID"])

        digest_algorithm_transform_map = {
            OneLogin_Saml2_Constants.SHA1: xmlsec.TransformSha1,
            OneLogin_Saml2_Constants.SHA256: xmlsec.TransformSha256,
            OneLogin_Saml2_Constants.SHA384: xmlsec.TransformSha384,
            OneLogin_Saml2_Constants.SHA512: xmlsec.TransformSha512
        }
        digest_algorithm_transform = digest_algorithm_transform_map.get(digest_algorithm, xmlsec.TransformSha1)

        ref = signature.addReference(digest_algorithm_transform)
        if elem_id:
            ref.attrib['URI'] = elem_id

        ref.addTransform(xmlsec.TransformEnveloped)
        ref.addTransform(xmlsec.TransformExclC14N)

        key_info = signature.ensureKeyInfo()
        key_info.addX509Data()

        dsig_ctx = xmlsec.DSigCtx()
        sign_key = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None)

        file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
        sign_key.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem)
        file_cert.close()

        dsig_ctx.signKey = sign_key
        dsig_ctx.sign(signature)

        return tostring(elem, encoding='unicode').encode('utf-8')
예제 #34
0
    def generate_name_id(value, sp_nq, sp_format=None, cert=None, debug=False, nq=None):
        """
        Generates a nameID.

        :param value: fingerprint
        :type: string

        :param sp_nq: SP Name Qualifier
        :type: string

        :param sp_format: SP Format
        :type: string

        :param cert: IdP Public Cert to encrypt the nameID
        :type: string

        :param debug: Activate the xmlsec debug
        :type: bool

        :param nq: IDP Name Qualifier
        :type: string

        :returns: DOMElement | XMLSec nameID
        :rtype: string
        """
        doc = Document()
        name_id_container = doc.createElementNS(OneLogin_Saml2_Constants.NS_SAML, 'container')
        name_id_container.setAttribute("xmlns:saml", OneLogin_Saml2_Constants.NS_SAML)

        name_id = doc.createElement('saml:NameID')
        if sp_nq is not None:
            name_id.setAttribute('SPNameQualifier', sp_nq)
        if nq is not None:
            name_id.setAttribute('NameQualifier', nq)
        if sp_format is not None:
            name_id.setAttribute('Format', sp_format)
        name_id.appendChild(doc.createTextNode(value))
        name_id_container.appendChild(name_id)

        if cert is not None:
            xml = name_id_container.toxml()
            elem = fromstring(xml, forbid_dtd=True)

            error_callback_method = None
            if debug:
                error_callback_method = print_xmlsec_errors
            xmlsec.set_error_callback(error_callback_method)

            # Load the public cert
            mngr = xmlsec.KeysMngr()
            file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
            key_data = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None)
            key_data.name = basename(file_cert.name)
            mngr.addKey(key_data)
            file_cert.close()

            # Prepare for encryption
            enc_data = EncData(xmlsec.TransformAes128Cbc, type=xmlsec.TypeEncElement)
            enc_data.ensureCipherValue()
            key_info = enc_data.ensureKeyInfo()
            # enc_key = key_info.addEncryptedKey(xmlsec.TransformRsaPkcs1)
            enc_key = key_info.addEncryptedKey(xmlsec.TransformRsaOaep)
            enc_key.ensureCipherValue()

            # Encrypt!
            enc_ctx = xmlsec.EncCtx(mngr)
            enc_ctx.encKey = xmlsec.Key.generate(xmlsec.KeyDataAes, 128, xmlsec.KeyDataTypeSession)

            edata = enc_ctx.encryptXml(enc_data, elem[0])

            newdoc = parseString(tostring(edata, encoding='unicode').encode('utf-8'), forbid_dtd=True)

            if newdoc.hasChildNodes():
                child = newdoc.firstChild
                child.removeAttribute('xmlns')
                child.removeAttribute('xmlns:saml')
                child.setAttribute('xmlns:xenc', OneLogin_Saml2_Constants.NS_XENC)
                child.setAttribute('xmlns:dsig', OneLogin_Saml2_Constants.NS_DS)

            nodes = newdoc.getElementsByTagName("*")
            for node in nodes:
                if node.tagName == 'ns0:KeyInfo':
                    node.tagName = 'dsig:KeyInfo'
                    node.removeAttribute('xmlns:ns0')
                    node.setAttribute('xmlns:dsig', OneLogin_Saml2_Constants.NS_DS)
                else:
                    node.tagName = 'xenc:' + node.tagName

            encrypted_id = newdoc.createElement('saml:EncryptedID')
            encrypted_data = newdoc.replaceChild(encrypted_id, newdoc.firstChild)
            encrypted_id.appendChild(encrypted_data)
            return newdoc.saveXML(encrypted_id)
        else:
            return doc.saveXML(name_id)
예제 #35
0
파일: base.py 프로젝트: victorlin/depot
 def encode(self):
     return lxml.tostring(self.to_element(ElementMaker(nsmap=self.nsmap)), xml_declaration=True, encoding="UTF-8")
예제 #36
0
 def xml(self) -> str:
     string = tostring(self.root, encoding='utf-8', pretty_print=True)
     return string.decode('utf-8')
예제 #37
0
    def add_sign(xml,
                 key,
                 cert,
                 debug=False,
                 sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1,
                 digest_algorithm=OneLogin_Saml2_Constants.SHA1):
        """
        Adds signature key and senders certificate to an element (Message or
        Assertion).

        :param xml: The element we should sign
        :type: string | Document

        :param key: The private key
        :type: string

        :param cert: The public
        :type: string

        :param debug: Activate the xmlsec debug
        :type: bool

        :param sign_algorithm: Signature algorithm method
        :type sign_algorithm: string

        :param digest_algorithm: Digest algorithm method
        :type digest_algorithm: string

        :returns: Signed XML
        :rtype: string
        """
        if xml is None or xml == '':
            raise Exception('Empty string supplied as input')
        elif isinstance(xml, etree._Element):
            elem = xml
        elif isinstance(xml, Document):
            xml = xml.toxml()
            elem = fromstring(xml.encode('utf-8'))
        elif isinstance(xml, Element):
            xml.setAttributeNS(unicode(OneLogin_Saml2_Constants.NS_SAMLP),
                               'xmlns:samlp',
                               unicode(OneLogin_Saml2_Constants.NS_SAMLP))
            xml.setAttributeNS(unicode(OneLogin_Saml2_Constants.NS_SAML),
                               'xmlns:saml',
                               unicode(OneLogin_Saml2_Constants.NS_SAML))
            xml = xml.toxml()
            elem = fromstring(xml.encode('utf-8'))
        elif isinstance(xml, basestring):
            elem = fromstring(xml.encode('utf-8'))
        else:
            raise Exception('Error parsing xml string')

        error_callback_method = None
        if debug:
            error_callback_method = print_xmlsec_errors
        xmlsec.set_error_callback(error_callback_method)

        # Sign the metadata with our private key.
        sign_algorithm_transform_map = {
            OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1,
            OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1,
            OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256,
            OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384,
            OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512
        }
        sign_algorithm_transform = sign_algorithm_transform_map.get(
            sign_algorithm, xmlsec.TransformRsaSha1)

        signature = Signature(xmlsec.TransformExclC14N,
                              sign_algorithm_transform)

        issuer = OneLogin_Saml2_Utils.query(elem, '//saml:Issuer')
        if len(issuer) > 0:
            issuer = issuer[0]
            issuer.addnext(signature)
        else:
            entity_descriptor = OneLogin_Saml2_Utils.query(
                elem, '//md:EntityDescriptor')
            if len(entity_descriptor) > 0:
                elem.insert(0, signature)
            else:
                elem[0].insert(0, signature)

        digest_algorithm_transform_map = {
            OneLogin_Saml2_Constants.SHA1: xmlsec.TransformSha1,
            OneLogin_Saml2_Constants.SHA256: xmlsec.TransformSha256,
            OneLogin_Saml2_Constants.SHA384: xmlsec.TransformSha384,
            OneLogin_Saml2_Constants.SHA512: xmlsec.TransformSha512
        }
        digest_algorithm_transform = digest_algorithm_transform_map.get(
            digest_algorithm, xmlsec.TransformSha1)

        ref = signature.addReference(digest_algorithm_transform)
        ref.addTransform(xmlsec.TransformEnveloped)
        ref.addTransform(xmlsec.TransformExclC14N)

        key_info = signature.ensureKeyInfo()
        key_info.addX509Data()

        dsig_ctx = xmlsec.DSigCtx()
        sign_key = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None)

        file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
        sign_key.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem)
        file_cert.close()

        dsig_ctx.signKey = sign_key
        dsig_ctx.sign(signature)

        newdoc = parseString(
            tostring(elem, encoding='unicode').encode('utf-8'))

        signature_nodes = newdoc.getElementsByTagName("Signature")

        for signature in signature_nodes:
            signature.removeAttribute('xmlns')
            signature.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS)
            if not signature.tagName.startswith('ds:'):
                signature.tagName = 'ds:' + signature.tagName
            nodes = signature.getElementsByTagName("*")
            for node in nodes:
                if not node.tagName.startswith('ds:'):
                    node.tagName = 'ds:' + node.tagName

        return newdoc.saveXML(newdoc.firstChild)
예제 #38
0
    def synchronize(self, soap_function="IPMSDataWebService", test=False):
        '''
        Runs the SynchronizationJob

        Args:
            soap_function (str) - The function to call on the SOAP client
            test (bool) - Indicates if we should use the testing method of the SOAP client
        '''
        start = datetime.datetime.utcnow()
        self.running = True
        self.save()
        try:
            logger = logging.getLogger('synchronization')
            logger.info(self)

            logger.info("Getting SOAP client")

            client = get_soap_client(soap_function=soap_function, test=test)

            logger.info(f"Using function {soap_function}")

            synchronization_tasks = get_synchronization_information(
                self.talentmap_model)
            for task in synchronization_tasks:
                logger.info(f"Running task {task.__name__}")
                soap_arguments, instance_tag, tag_map, collision_field, post_load_function, override_loading_method = task(
                )
                model = apps.get_model(self.talentmap_model)

                logger.info("Intializing XML loader")

                loader = XMLloader(model, instance_tag, tag_map, 'update',
                                   collision_field, override_loading_method)

                logger.info("Loader initialized, pulling XML data")
                '''
                The XML data from DOS SOAP comes back in batches, we need to use the last 'collision_field'
                as the PaginationStartKey of the next request, and continue until we get no more results
                '''
                new_ids = []
                updated_ids = []
                last_collision_field = None
                soap_function_name = soap_function
                while True:
                    if last_collision_field:
                        # Set the pagination start key to our last collision field; which should be the remote data's primary key
                        soap_arguments[
                            'PaginationStartKey'] = last_collision_field
                        logger.info(
                            f"Requesting page from primary key: {last_collision_field}"
                        )
                    else:
                        logger.info(f"Requesting first page")

                    # Get the data
                    response_xml = ET.tostring(getattr(
                        client.service, soap_function_name)(**soap_arguments),
                                               encoding="unicode")

                    newer_ids, updateder_ids, last_collision_field = loader.create_models_from_xml(
                        response_xml, raw_string=True)

                    # If there are no new or updated ids on this page, we've reached the end
                    if len(newer_ids) == 0 and len(updateder_ids) == 0:
                        break

                    logger.info(
                        f"Got: {len(newer_ids)} new, {len(updateder_ids)} updated this page"
                    )

                    # Append our newer and updated-er ids to our total list
                    new_ids = new_ids + newer_ids
                    updated_ids = updated_ids + updateder_ids

            logger.info("Data pull complete")

            # Run the post load function, if it exists
            if callable(post_load_function):
                logger.info(f"Calling post-load function for model {model}")
                post_load_function(model, new_ids, updated_ids)

            logger.info("Synchronization complete")
            d = relativedelta(start, datetime.datetime.utcnow())
            logger.info(
                f"Synchronization Report\n\tModel: {self.talentmap_model}\n\tNew: {len(new_ids)}\n\tUpdated: {len(updated_ids)}\n\tElapsed time: {d.days} d {d.minutes} min {d.seconds} s\t\t"
            )

            # Successful, set the last synchronization
            self.last_synchronization = timezone.now()
        except:  # pragma: no cover
            raise
        finally:
            self.running = False
            self.save()