Esempio n. 1
0
 def test_session_expiration(self):
     exc = _get_exception_class_for(HealthVaultStatus.AUTHENTICATED_SESSION_TOKEN_EXPIRED)
     self.assertEqual(HealthVaultTokenExpiredException, exc)
Esempio n. 2
0
 def test_access_denied(self):
     exc = _get_exception_class_for(HealthVaultStatus.ACCESS_DENIED)
     self.assertEqual(HealthVaultAccessDeniedException, exc)
Esempio n. 3
0
 def test_token_expiration(self):
     exc = _get_exception_class_for(HealthVaultStatus.CREDENTIAL_TOKEN_EXPIRED)
     self.assertEqual(HealthVaultTokenExpiredException, exc)
Esempio n. 4
0
 def test_access_denied(self):
     exc = _get_exception_class_for(HealthVaultStatus.ACCESS_DENIED)
     self.assertEqual(HealthVaultAccessDeniedException, exc)
Esempio n. 5
0
 def test_session_expiration(self):
     exc = _get_exception_class_for(HealthVaultStatus.AUTHENTICATED_SESSION_TOKEN_EXPIRED)
     self.assertEqual(HealthVaultTokenExpiredException, exc)
Esempio n. 6
0
 def test_token_expiration(self):
     exc = _get_exception_class_for(HealthVaultStatus.CREDENTIAL_TOKEN_EXPIRED)
     self.assertEqual(HealthVaultTokenExpiredException, exc)
Esempio n. 7
0
class HealthVaultConn(object):
    """A HealthVaultConn object is used to access data for one patient ("record").

    When the HealthVaultConn object is created, it connects to the server to verify the credentials it was given,
    and retrieve the record ID corresponding to the WCTOKEN.

    Often you won't have the WCTOKEN yet. Leave it out and the HealthVaultConn object will get an
    authorized session to HealthVault but not yet get the record ID.

    To get a WCTOKEN, also known as the `user auth token`, your web application needs to redirect
    the user to HealthVault to grant your application authorization to access their data. You can
    use :py:meth:`.authorization_url` to get the full URL to redirect the user to. When
    that's done, HealthVault will redirect the user to your URL (that you passed to :py:meth:`.authorization_url`)
    and add query parameters including the auth token. Your app needs to accept that request and
    parse it for the user auth token.

    Then call :py:meth:`.connect` passing the user's auth token, and HealthVaultConn will verify access and
    retrieve the record id and person id for the record and person that the user has granted
    access to.

    Check the record id (:py:attr:`.record_id`). The user could change the person (patient)
    they're granting access to, and this is the only way for an application to tell
    that this `HealthVaultConn` object is now accessing data
    for a different person.

    When constructing a new `HealthVaultConn` for the first time, you will leave out `wctoken`,
    `sharedsec`, `auth_token`, and `record_id` because they aren't known yet.

    Once a `HealthVaultConn` has been constructed successfully, it will have established authentication
    with HealthVault. You can save the sharedsec and auth_token attributes and re-use them when constructing
    future `HealthVaultConn` objects to skip the original authentication call.

    Once a `HealthVaultConn` has been successfully connected to a particular patient's data (`.connect`
    called, or wctoken passed in the constructor successfully), you can
    additionally save the `wctoken` and `record_id` attributes and re-use them when constructing
    future `HealthVaultConn` objects that will access the same person's data. However, be careful
    if the wctoken expires and you get a new one, it might be pointing at a different person's data
    with a different record_id. When a wctoken is found to be not valid, it's safest to set the `record_id`
    attribute to None or create a new `HealthVaultConn` without passing a record_id, so the new record_id
    will be retrieved.

    These parameters are related to your application and should not generally change:

    :param string app_id: the application ID (UUID)
    :param string app_thumbprint: the thumbprint displayed in the ACC for the public key we're using
        (40 hex digits)
    :param long public_key: the public key we're using (a very long number)
    :param long private_key: the private key we're using (a very long number)
    :param string server: (optional), the hostname of the server to connect to, defaults to
        "platform.healthvault-ppe.com", the pre-production US server
    :param string shell_server: (optional), the hostname of the shell redirect server to connect to, defaults to
        "account.healthvault-ppe.com", the pre-production US shell server

    These parameters can be used to save re-establishing authentication with HealthVault, but need to
    be saved from another object. One application can use these for all `HealthVaultConn` objects:

    :param string sharedsec: a random string that HealthVaultConn generates if none is passed in. If you save
       an auth_token, you need to save this with it and pass them both into any new HealthVaultConn that you
       want to use them. (string containing a long integer, 20 chars or more)
    :param string auth_token: a long, random-looking string given to us by HealthVault when we authenticate
       our application with them. It's used along with the other cryptographic data in later calls. If you save
       this, save the sharedsec that goes with it and pass them both into any new HealthVaultConn that you
       want to use them.  (240 printable ASCII chars)

    These parameters can be used to save re-establishing authorization for a particular patient's
    data,  but need to be saved from another object. These are specific to accessing one person's
    data:

    :param string wctoken: the token returned from APPAUTH. If not available, leave it out and call
       :py:meth:`.connect(wctoken)` later.  (200 printable ASCII chars)
    :param string record_id: if you already know the wctoken and have saved the corresponding record_id, you can
       pass the record_id along with the wctoken to save a network call to look up the record_id.  Note that if
       the wctoken is found to be invalid (probably expired), the record_id might not be correct when you get a
       new wctoken, so you should set your HealthVaultConn.record_id back to None before getting the new wctoken.
       (UUID)

    :raises: :py:exc:`HealthVaultException` if there's any problem connecting to HealthVault or getting authorized.
    """

    record_id = None
    """
    The HealthVault record ID corresponding to the auth-token.  A string containing a UUID, or None.
    This identifies uniquely the person whose data we are accessing.
    """
    def __init__(self,
                 app_id,
                 app_thumbprint,
                 public_key,
                 private_key,
                 server=None,
                 shell_server=None,
                 sharedsec=None,
                 auth_token=None,
                 wctoken=None,
                 record_id=None):
        self.wctoken = wctoken
        self.app_id = app_id
        self.app_thumbprint = app_thumbprint
        self.public_key = public_key
        self.private_key = private_key
        # Default to the US, pre-production servers
        self.server = server or 'platform.healthvault-ppe.com'
        self.shell_server = shell_server or "account.healthvault-ppe.com"

        self.sharedsec = sharedsec or str(randint(2**64, 2**65 - 1))

        self.record_id = record_id

        self.authorized = False

        if not isinstance(public_key, long):
            raise ValueError("public key must be a long; it's %r" % public_key)
        if not isinstance(private_key, long):
            raise ValueError("public key must be a long; it's %r" %
                             private_key)

        # We can get our auth token now, it's not specific to wctoken
        # This will catch it early if our keys are wrong or something like that
        self.auth_token = auth_token or self._get_auth_token()

        if wctoken:
            self.connect(wctoken)

    def is_authorized(self):
        """Return True if we've been authorized to HealthVault for a user.
        If not, :py:meth:`.connect()` needs to be called before attempting online access.
        Offline access might still be possible.
        """
        return self.authorized

    def connect(self, wctoken):
        """Set the wctoken (user auth token) to use, and establish an authorized session with HealthVault
        that can access this person's data. You don't need to call this if a wctoken
        was passed initially.

        :param string wctoken: The auth token passed to the application after the user has
            authorized the app.  Specifically, this is the value of the `wctoken`
            query parameter on that request.

        :raises: HealthVaultException if there's any problem connecting to HealthVault
            or getting authorized.
        """
        self.wctoken = wctoken
        self.record_id = self._get_record_id()
        self.authorized = True

    def authorization_url(self, callback_url=None, record_id=None):
        """Return the URL that the user needs to be redirected to in order to
        grant authorization to this app to access their data.

        *The callback_url parameter is only valid during development*. The production server will
        always redirect the user to the application's configured ActionURL.
        It might also fail the request if a callback URL is even passed.

        :note: Use a 307 (temporary) redirect. The user's browser might cache
            a 301 (permanent) redirect, resulting in the user not being able to
            get back to the original page because their browser keeps redirecting
            them to HealthVault due to the cached redirect for that URL.

        :param string record_id: Optionally request access to a particular person's
            (patient's) data.  If this is not passed and this `HealthVaultConn` object
            has a record_id associated with it, that will be used. (UUID)

        :param URL callback_url: The URL that the user will be redirected back to after
            they have finished interacting with HealthVault. It will have query
            parameters appended by HealthVault indicating whether the authorization was
            granted, and providing the wctoken value if so.  See also
            :py:meth:`connect`.  **THIS ONLY WORKS WITH PRE-PRODUCTION HEALTHVAULT
            SERVERS. PRODUCTION HEALTHVAULT SERVERS WILL ALWAYS REDIRECT TO THE
            APPLICATIONS `ActionURL` AS CONFIGURED IN HEALTHVAULT.**

        See `APPAUTH <http://msdn.microsoft.com/en-us/library/ff803620.aspx#APPAUTH>`_.
        """
        d = {'appid': self.app_id}
        if callback_url is not None:
            d['redirect'] = callback_url
        record_id = record_id or self.record_id
        if record_id is not None:
            d['extrecordid'] = record_id
        targetqs = urlencode(d)
        return "https://%s/redirect.aspx?%s" % (self.shell_server,
                                                urlencode({
                                                    'target': "APPAUTH",
                                                    'targetqs': targetqs
                                                }))

    def deauthorization_url(self, callback_url=None):
        """Return the URL that the user needs to be redirected to in order to
        cancel their authorization for this app to access their data. Useful
        for a logout action.

        HealthVault will redirect the user to your application's ActionURL
        with a `target` parameter of `SIGNOUT`.  During development only,
        a different URL may be used by passing it as `callback_url`.

        **The callback_url parameter is only valid during development**. The production server will
        always redirect the user to the application's configured ActionURL.
        It might also fail the request if a callback URL is even passed.

        :param URL callback_url: The URL that the user will be redirected back to after
            they have finished interacting with HealthVault.   **THIS ONLY WORKS WITH
            PRE-PRODUCTION HEALTHVAULT SERVERS. PRODUCTION HEALTHVAULT SERVERS WILL
            ALWAYS REDIRECT TO THE APPLICATIONS `ActionURL` AS CONFIGURED IN HEALTHVAULT.**

        See `APPSIGNOUT <http://msdn.microsoft.com/en-us/library/ff803620.aspx#APPSIGNOUT>`_.
        """
        d = {'appid': self.app_id, 'cred_token': self.auth_token}
        if callback_url is not None:
            d['redirect'] = callback_url
        targetqs = urlencode(d)
        return "https://%s/redirect.aspx?%s" % (self.shell_server,
                                                urlencode({
                                                    'target': "APPSIGNOUT",
                                                    'targetqs': targetqs
                                                }))

    def _get_auth_token(self):
        """Call HealthVault and get a session token, returning it.

        Not part of the public API, just factored out of __init__ for testability.
        """

        # Interesting note: wctoken is not needed here. The token we're getting is just
        # for our app and is not specific to a particular user.

        crypto = HVCrypto(self.public_key, self.private_key)

        sharedsec64 = base64.encodestring(self.sharedsec)

        content = '<content>'\
                      '<app-id>' + self.app_id + '</app-id>'\
                      '<shared-secret>'\
                          '<hmac-alg algName="HMACSHA1">' + sharedsec64 + '</hmac-alg>'\
                      '</shared-secret>'\
                  '</content>'
        #3. create header
        header = "<header>"\
                     "<method>CreateAuthenticatedSessionToken</method>"\
                     "<method-version>1</method-version>"\
                     "<app-id>" + self.app_id + "</app-id>"\
                     "<language>en</language><country>US</country>"\
                     "<msg-time>2008-06-21T03:13:50.750-04:00</msg-time>"\
                     "<msg-ttl>36000</msg-ttl>"\
                     "<version>" + HEALTHVAULT_VERSION + "</version>"\
                 "</header>"
        self.signature = crypto.sign(content)
        #4. create info with signed content
        info = '<info>'\
                   '<auth-info>'\
                       '<app-id>' + self.app_id + '</app-id>'\
                       '<credential>'\
                            '<appserver>'\
                                '<sig digestMethod="SHA1" sigMethod="RSA-SHA1" thumbprint="' + self.app_thumbprint + '">'\
                                       + self.signature +\
                                '</sig>'\
                                + content +\
                            '</appserver>'\
                       '</credential>'\
                   '</auth-info>'\
               '</info>'
        payload = '<wc-request:request xmlns:wc-request="urn:com.microsoft.wc.request">' + header + info + '</wc-request:request>'

        (response, body, tree) = self._send_request(payload)

        key = '{urn:com.microsoft.wc.methods.response.CreateAuthenticatedSessionToken}info'
        info = tree.find(key)
        if info is None:
            raise HealthVaultException("No %s in response (%s)" % (key, body))
        token_elt = info.find('token')
        if token_elt is None:
            logger.error(
                "No session token in response.  Request=%s.  Response=%s" %
                (payload, body))
            raise HealthVaultException(
                "Something wrong in response from HealthVault getting session token -"
                " no token in response (%s)" % body)
        return token_elt.text

    def _get_record_id(self):
        """
        Calls GetPersonInfo, returns selected_record_id

        If this `HealthVaultConn` already has a record_id, just returns that.

        Not part of the public API.
        """
        if self.record_id:
            return self.record_id

        (response, body,
         tree) = self._build_and_send_request("GetPersonInfo",
                                              "<info/>",
                                              use_record_id=False)

        record_id_elt = tree.find(
            '{urn:com.microsoft.wc.methods.response.GetPersonInfo}info/person-info/selected-record-id'
        )
        if record_id_elt is None:
            logger.error("No record ID in response.  response=%s" % body)
            raise HealthVaultException(
                "selected record ID not found in HV response (%s)" % body)

        return record_id_elt.text

    def _send_request(self, payload):
        """
        Send payload as a request to the HealthVault API.

        :returns: (response, body, elementtree)

        Contents of return value::

            response
                HTTPResponse

            body
                string with body of response

            elementtree
                ElementTree.Element object with parsed body

        :param string payload: The request body
        :raises: HealthVaultException if HTTP response status is not 200 or status in parsed response is not 0.

        Not part of the public API.
        """
        conn = httplib.HTTPSConnection(self.server, 443)
        conn.putrequest('POST', '/platform/wildcat.ashx')
        conn.putheader('Content-Type', 'text/xml')
        conn.putheader('Content-Length', '%d' % len(payload))
        conn.endheaders()
        #logger.debug("Posting request: %s" % payload)
        try:
            conn.send(payload)
        except socket.error, v:
            if v[0] == 32:  # Broken pipe
                conn.close()
            raise
        response = conn.getresponse()
        if response.status != 200:
            logger.error(
                "Non-success HTTP response status from HealthVault.  Status=%d, message=%s"
                % (response.status, response.reason))
            raise HealthVaultHTTPException(
                "Non-success HTTP response status from HealthVault.  Status=%d, message=%s"
                % (response.status, response.reason),
                code=response.status)
        body = response.read()
        tree = ET.fromstring(body)
        status = int(tree.find('status/code').text)
        if status != 0:
            msg = tree.find("status/error/message").text
            logger.error(
                "HealthVault error. status=%d, message=%s, request=%s, response=%s"
                % (status, msg, pretty_xml(payload), pretty_xml(body)))
            exc_class = _get_exception_class_for(status)
            raise exc_class(
                "Non-success status from HealthVault API.  Status=%d, message=%s"
                % (status, msg),
                code=status)
        #logger.debug("response body=%r" % body)
        return (response, body, tree)