pylero.session — pylero documentation

# Source code for pylero.session

    # -*- coding: utf8 -*-
    from __future__ import absolute_import, division, print_function, unicode_literals

    import logging
    import re
    import ssl
    import time

    import suds.client
    import suds.sax.element
    from suds.plugin import MessagePlugin
    from suds.sax.attribute import Attribute

    from pylero._compatible import (
        builtins,  # noqa
        object,
        urlparse,
    )

    logger = logging.getLogger(__name__)
    # We create a logger in order to intercept log records from suds.client
    suds_logger = logging.getLogger("suds.client")

    CERT_PATH = None

    # Regular expression to catch SOAP message containing password field
    REGEX_SOAP_MESSAGE_PASSWORD_FIELD = re.compile(r"(<ns\d:password>)(.*)(</ns\d:password>).*")

    class ListenFilter(logging.Filter):

        def filter(self, record):
            """Determine which log records to output.
            Returns 0 for no, nonzero for yes.
            """
            # We assume that this message contains the password in plaintext
            # which as part of SOAP message
            if "<ns" and ":password" in record.getMessage():
                masked_record = re.sub(REGEX_SOAP_MESSAGE_PASSWORD_FIELD, r"\1*********\3", record.getMessage())
                logger.critical(masked_record)
                return False
            return True

    suds_logger.addFilter(ListenFilter())

    # the reason why this function definition is at the top is because it is
    # assigned to "ssl._create_default_https_context", few lines below

    def create_ssl_context():
        """this function creates a custom ssl context which is required for ssl
        connection in python-version >=2.7.10. this ssl context is customize to use
        certificate which is located in 'CERT_PATH'.
        """
        context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
        context.verify_mode = ssl.CERT_REQUIRED
        context.check_hostname = True
        context.load_verify_locations(CERT_PATH)
        return context

    class SoapNull(MessagePlugin):
        """suds plugin that is called before any suds message is sent to the remote
        server. It adds the xsi:nil=true attribute to any element that is blank.
        Without this plugin, a number of functions that were supposed to accept
        null parameters did not work.
        """

        def marshalled(self, context):
            # Go through every node in the document and check if it is empty and
            # if so set the xsi:nil tag to true
            context.envelope.walk(self.add_nil)

        def add_nil(self, element):
            """Used as a filter function with walk to add xsi:nil to blank attrs."""
            if element.isempty() and not element.isnil():
                element.attributes.append(Attribute("xsi:nil", "true"))

    class Session(object):
        def _url_for_name(self, service_name):
            """generate the full URL for the WSDL client services"""
            return "{0}/ws/services/{1}WebService?wsdl".format(self._server.url, service_name)

        def __init__(self, server, timeout):
            """Session constructor, initialize the WSDL clients

            Args:
                 server: server object that the session connects to
                 caching_policy: determines the caching policy of the SUDS conn
                 timeout: HTTP timeout for the connection
            """
            self._server = server
            self._last_request_at = None
            self._session_id_header = None
            self._cookies = None

            # This block forces ssl certificate verification
            # Must be set BEFORE creating any WSDL clients (fixes #185)
            if self._server.cert_path:
                global CERT_PATH
                CERT_PATH = self._server.cert_path
                ssl._create_default_https_context = create_ssl_context

            self._session_client = _SudsClientWrapper(self._url_for_name("Session"), None, timeout)
            self.builder_client = _SudsClientWrapper(self._url_for_name("Builder"), self, timeout)
            self.planning_client = _SudsClientWrapper(self._url_for_name("Planning"), self, timeout)
            self.project_client = _SudsClientWrapper(self._url_for_name("Project"), self, timeout)
            self.security_client = _SudsClientWrapper(self._url_for_name("Security"), self, timeout)
            self.test_management_client = _SudsClientWrapper(self._url_for_name("TestManagement"), self, timeout)
            self.tracker_client = _SudsClientWrapper(self._url_for_name("Tracker"), self, timeout)

        def _login(self):
            """login to the Polarion API"""
            sc = self._session_client
            if self._server.token:
                sc.service.logInWithToken("AccessToken", None, self._server.token)
            else:
                sc.service.logIn(self._server.user, self._server.password)
            id_element = sc.last_received().childAtPath("Envelope/Header/sessionID")
            session_id = id_element.text
            session_ns = id_element.namespace()
            self._session_id_header = suds.sax.element.Element("sessionID", ns=session_ns).setText(session_id)
            self._cookies = sc.options.transport.cookiejar
            sc.set_options(soapheaders=self._session_id_header)
            self._last_request_at = time.time()

        def _logout(self):
            """logout from Polarion server"""
            self._session_client.service.endSession()

        def _reauth(self):
            """auto relogin after timeout, set in the getattr function of each
            client obj
            """
            sc = self._session_client
            duration = time.time() - self._last_request_at
            if duration > self._server.relogin_timeout and not sc.service.hasSubject():
                logger.debug("Session expired, trying to log in again")
                self._login()
            else:
                self._last_request_at = time.time()

        def tx_begin(self):
            self._session_client.service.beginTransaction()

        def tx_commit(self):
            self._session_client.service.endTransaction(False)

        def tx_rollback(self):
            self._session_client.service.endTransaction(True)

        def tx_release(self):
            if self._session_client.service.transactionExists():
                self.tx_rollback()

        def tx_in(self):
            """Function checks if a transaction is in progress. You can not have a
            transaction within another transaction. This function helps the system
            determine if it should start a new transaction or if it is already in
            the middle of one.

            Returns:
                bool
            """
            return self._session_client.service.transactionExists()

    class _SudsClientWrapper(object):
        """class that manages the WSDL clients"""

        def __init__(self, url, enclosing_session, timeout):
            """has the actual WSDL client as a private _suds_client attribute so
            that the "magic" __getattr__ function will be able to verify
            functions called on it and after processing to call the WSDL function

            Args:
                url (str): the URL of the Polarion server.
                enclosing_session: the HTTP session that the requests are sent
                                   through
                timeout (int): The HTTP timeout of the connection
            """
            plugin = SoapNull()
            self._suds_client = suds.client.Client(url, plugins=[plugin], timeout=timeout)
            self._enclosing_session = enclosing_session

        def __getattr__(self, attr):
            # every time a client function is called, this verifies that there is
            # still an active connection and if not, it reconnects.
            logger.debug("attr={0} self={1}".format(attr, self.__dict__))
            if attr == "service" and self._enclosing_session and self._enclosing_session._session_id_header is not None:
                logger.debug("Calling hook before _suds_client_wrapper.service " "access")
                self._enclosing_session._reauth()
                self._suds_client.set_options(soapheaders=self._enclosing_session._session_id_header)
                # for some reason adding the cookiejar didn't work, so the
                # cookie is being added to the header manually.
                # self._suds_client.options.transport.cookiejar = \
                #    self._enclosing_session._cookies
                # adding the RouteID cookie, if it exists to the headers.
                hostname = urlparse(self._enclosing_session._server.url).hostname
                route = self._enclosing_session._cookies._cookies.get(hostname, {}).get("/", {}).get("ROUTEID")
                if route:
                    self._suds_client.options.headers["Cookie"] = "ROUTEID=%s" % route.value
            return getattr(self._suds_client, attr)