Logo Search packages:      
Sourcecode: ubuntuone-client version File versions

auth.py

# ubuntuone.oauthdesktop.auth - Client authorization module
#
# Author: Stuart Langridge <stuart.langridge@canonical.com>
#
# Copyright 2009 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE.  See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.
"""OAuth client authorisation code.

This code handles acquisition of an OAuth access token for a service,
managed through the GNOME keyring for future use, and asynchronously.
"""

__metaclass__ = type

import subprocess
import random
import pycurl
import StringIO
import dbus

import gnomekeyring
from ubuntuone.storageprotocol import oauth
from ubuntuone.oauthdesktop.key_acls import set_all_key_acls

from twisted.internet import reactor
from twisted.web import server, resource

from ubuntuone.oauthdesktop.logger import setupLogging
setupLogging()
import logging
logger = logging.getLogger("UbuntuOne.OAuthDesktop.auth")


00045 class NoAccessToken(Exception):
    """No access token available."""

00048 class ConsumerKeyInvalid(Exception):
    """OAuth said the consumer key was invalid."""
00050 class UnknownLoginError(Exception):
    """An OAuth request failed for some reason."""
00052 class NotOnlineError(Exception):
    """There is no network connection."""

# NetworkManager State constants
NM_STATE_UNKNOWN = 0
NM_STATE_ASLEEP = 1
NM_STATE_CONNECTING = 2
NM_STATE_CONNECTED = 3
NM_STATE_DISCONNECTED = 4

00062 class AuthorisationClient(object):
    """OAuth authorisation client."""
00064     def __init__(self, realm, request_token_url, user_authorisation_url,
                 access_token_url, consumer_key, consumer_secret,
                 callback_parent, callback_denied=None, do_login=True,
                 keyring=gnomekeyring):
        """Create an `AuthorisationClient` instance.

        @param realm: the OAuth realm.
        @param request_token_url: the OAuth request token URL.
        @param user_authorisation_url: the OAuth user authorisation URL.
        @param access_token_url: the OAuth access token URL.
        @param consumer_key: the OAuth consumer key.
        @param consumer_secret: the OAuth consumer secret.
        @param callback_parent: a function in the includer to call with a token

        The preceding parameters are defined in sections 3 and 4.1 of the
        OAuth Core 1.0 specification.  The following parameters are not:

        @param callback_denied: a function to call if no token is available
        @param do_login: whether to create a token if one is not cached
        @param keychain: the keyring object to use (defaults to gnomekeyring)

        """
        self.realm = realm
        self.request_token_url = request_token_url
        self.user_authorisation_url = user_authorisation_url
        self.access_token_url = access_token_url
        self.consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
        self.callback_parent = callback_parent
        self.callback_denied = callback_denied
        self.do_login = do_login
        self.request_token = None
        self.saved_acquire_details = (None, None, None)
        self.keyring = keyring
        logger.debug("auth.AuthorisationClient created with parameters "+ \
           "realm='%s', request_token_url='%s', user_authorisation_url='%s',"+\
           "access_token_url='%s', consumer_key='%s', callback_parent='%s'",
           realm, request_token_url, user_authorisation_url, access_token_url,
           consumer_key, callback_parent)

00103     def _get_keyring_items(self):
        """Raw interface to obtain keyring items."""
        return self.keyring.find_items_sync(gnomekeyring.ITEM_GENERIC_SECRET,
                                            {'ubuntuone-realm': self.realm,
                                             'oauth-consumer-key':
                                             self.consumer.key})

00110     def get_access_token(self):
        """Get the access token from the keyring.

        If no token is available in the keyring, `NoAccessToken` is raised.
        """
        logger.debug("Trying to fetch the token from the keyring")
        try:
            items = self._get_keyring_items()
        except (gnomekeyring.NoMatchError,
                gnomekeyring.DeniedError):
            logger.debug("Access token was not in the keyring")
            raise NoAccessToken("No access token found.")
        logger.debug("Access token successfully found in the keyring")
        return oauth.OAuthToken.from_string(items[0].secret)

00125     def clear_token(self):
        """Clear any stored tokens from the keyring."""
        logger.debug("Searching keyring for existing tokens to delete.")
        try:
            items = self._get_keyring_items()
        except (gnomekeyring.NoMatchError,
                gnomekeyring.DeniedError):
            logger.debug("No preexisting tokens found")
        else:
            logger.debug("Deleting %s tokens from the keyring" % len(items))
            for item in items:
                try:
                    self.keyring.item_delete_sync(None, item.item_id)
                except gnomekeyring.DeniedError:
                    logger.debug("Permission denied deleting token")

00141     def store_token(self, access_token):
        """Store the given access token in the keyring.

        The keyring item is identified by the OAuth realm and consumer
        key to support multiple instances.
        """
        logger.debug("Trying to store the token in the keyring")
        item_id = self.keyring.item_create_sync(
            None,
            gnomekeyring.ITEM_GENERIC_SECRET,
            'UbuntuOne token for %s' % self.realm,
            {'ubuntuone-realm': self.realm,
             'oauth-consumer-key': self.consumer.key},
            access_token.to_string(),
            True)

        # set ACLs on the key for all apps listed in xdg BaseDir, but only
        # the root level one, not the user-level one
        logger.debug("Setting ACLs on the token in the keyring")
        set_all_key_acls(item_id=item_id)

        # keyring seems to take a while to actually apply the change
        # for when other people retrieve it, so sleep a bit.
        # this ought to get fixed.
        import time
        time.sleep(4)

00168     def have_access_token(self):
        """Returns true if an access token is available from the keyring."""
        try:
            self.get_access_token()
        except NoAccessToken:
            return False
        else:
            return True

00177     def make_token_request(self, oauth_request):
        """Perform the given `OAuthRequest` and return the associated token."""
        # uses pycurl because that will fail on self-signed certs

        logger.debug("Making a token request")
        accum = StringIO.StringIO()
        c = pycurl.Curl()
        c.setopt(c.URL, str(oauth_request.http_url)) # no unicode
        c.setopt(c.WRITEFUNCTION, accum.write)
        c.setopt(c.POSTFIELDS, oauth_request.to_postdata())
        try:
            c.perform()
        except pycurl.error, e:
            logger.debug("There was some unknown login error '%s'", e)
            raise UnknownLoginError(e.message)
        c.close()
        accum.seek(0)
        data = accum.read()
        # we deliberately trap anything that might go wrong when parsing the
        # token, because we do not want this to explicitly fail
        # pylint: disable-msg=W0702
        try:
            out_token = oauth.OAuthToken.from_string(data)
            logger.debug("Token successfully requested")
            return out_token
        except:
            logger.debug("Token was not successfully retrieved: data was '%s'",
               data)

00206     def open_in_browser(self, url):
        """Open the given URL in the user's web browser."""
        logger.debug("Opening '%s' in the browser", url)
        ret = subprocess.call(["xdg-open", url])
        if ret != 0:
            raise Exception("Failed to launch browser")

00213     def acquire_access_token_if_online(self, description=None, store=False):
        """Check to see if we are online before trying to acquire"""
        # Get NetworkManager state
        logger.debug("Checking whether we are online")
        try:
            nm = dbus.SystemBus().get_object('org.freedesktop.NetworkManager',
                                             '/org/freedesktop/NetworkManager')
        except dbus.exceptions.DBusException:
            logger.warn("Unable to connect to NetworkManager. Trying anyway.")
            self.acquire_access_token(description, store)
        else:
            iface = dbus.Interface(nm, 'org.freedesktop.NetworkManager')

            def got_state(state):
                """Handler for when state() call succeeds."""
                if state == NM_STATE_CONNECTED:
                    logger.debug("We are online")
                    self.acquire_access_token(description, store)
                elif state == NM_STATE_CONNECTING:
                    logger.debug("We are currently going online")
                    # attach to NM's StateChanged signal
                    signal_match = nm.connect_to_signal(
                        signal_name="StateChanged",
                        handler_function=self.connection_established,
                        dbus_interface="org.freedesktop.NetworkManager")
                    # stash the details so the handler_function can get at them
                    self.saved_acquire_details = (signal_match, description,
                                                  store)
                else:
                    # NM is not connected: fail
                    logger.debug("We are not online")
                    raise NotOnlineError()

            def got_error():
                """Handler for D-Bus errors when calling state()."""
                logger.debug("Received D-Bus error")
                raise NotOnlineError()

            iface.state(reply_handler=got_state, error_handler=got_error)

00253     def connection_established(self, state):
        """NetworkManager's state has changed, and we're watching for
           a connection"""
        logger.debug("Online status has changed to %s" % state)
        if int(state) == NM_STATE_CONNECTED:
            signal_match, description, store = self.saved_acquire_details
            # disconnect the signal so we don't get called again
            signal_match.remove()
            # call the real acquire_access_token now it has a connection
            logger.debug("Correctly connected: now starting auth process")
            self.acquire_access_token(description, store)
        else:
            # connection changed but not to "connected", so keep waiting
            logger.debug("Not yet connected: continuing to wait")

00268     def acquire_access_token(self, description=None, store=False):
        """Create an OAuth access token authorised against the user."""
        signature_method = oauth.OAuthSignatureMethod_PLAINTEXT()

        # Create a request token ...
        logger.debug("Creating a request token to begin access request")
        parameters = {}
        if description:
            parameters['description'] = description
        oauth_request = oauth.OAuthRequest.from_consumer_and_token(
            http_url=self.request_token_url,
            oauth_consumer=self.consumer,
            parameters=parameters)
        oauth_request.sign_request(signature_method, self.consumer, None)
        logger.debug("Making token request")
        self.request_token = self.make_token_request(oauth_request)

        # Request authorisation from the user
        # Add a nonce to the query so we know the callback (to our temp
        # webserver) came from us
        nonce = random.randint(1000000, 10000000)

        # start temporary webserver to receive browser response
        callback_url = self.get_temporary_httpd(nonce,
           self.retrieve_access_token, store)
        oauth_request = oauth.OAuthRequest.from_token_and_callback(
            http_url=self.user_authorisation_url,
            token=self.request_token,
            callback=callback_url)
        self.open_in_browser(oauth_request.to_url())

    def get_temporary_httpd(self, nonce, retrieve_function, store):
        "A separate class so it can be mocked in testing"
        logger.debug("Creating a listening temp web server")
        site = TemporaryTwistedWebServer(nonce=nonce,
          retrieve_function=retrieve_function, store_yes_no=store)
        temphttpd = server.Site(site)
        temphttpdport = reactor.listenTCP(0, temphttpd)
        callback_url = "http://127.0.0.1:%s/?nonce=%s" % (
          temphttpdport.getHost().port, nonce)
        site.set_port(temphttpdport)
        logger.debug("Webserver listening on port '%s'", temphttpdport)
        return callback_url

00312     def retrieve_access_token(self, store=False):
        """Retrieve the access token, once OAuth is done. This is a callback."""
        logger.debug("Access token callback from temp webserver")
        signature_method = oauth.OAuthSignatureMethod_PLAINTEXT()
        oauth_request = oauth.OAuthRequest.from_consumer_and_token(
            http_url=self.access_token_url,
            oauth_consumer=self.consumer,
            token=self.request_token)
        oauth_request.sign_request(
            signature_method, self.consumer, self.request_token)
        logger.debug("Retrieving access token from OAuth")
        access_token = self.make_token_request(oauth_request)
        if not access_token:
            logger.debug("Failed to get access token.")
            if self.callback_denied is not None:
                self.callback_denied()
        else:
            if store:
                logger.debug("Storing access token in keyring")
                self.store_token(access_token)
            logger.debug("Calling the callback_parent")
            self.callback_parent(access_token)

00335     def ensure_access_token(self, description=None):
        """Returns an access token, either from the keyring or newly acquired.

        If a new token is acquired, it will be stored in the keyring
        for future use.
        """
        try:
            access_token = self.get_access_token()
            self.callback_parent(access_token)
        except NoAccessToken:
            if self.do_login:
                access_token = self.acquire_access_token_if_online(
                    description,
                    store=True)
            else:
                if self.callback_denied is not None:
                    self.callback_denied()


00354 class TemporaryTwistedWebServer(resource.Resource):
    """A temporary httpd for the oauth process to call back to"""
    isLeaf = True
00357     def __init__(self, nonce, store_yes_no, retrieve_function):
        """Initialize the temporary web server."""
        resource.Resource.__init__(self)
        self.nonce = nonce
        self.store_yes_no = store_yes_no
        self.retrieve_function = retrieve_function
        reactor.callLater(600, self.stop) # ten minutes
        self.port = None
00365     def set_port(self, port):
        """Save the Twisted port object so we can stop it later"""
        self.port = port
00368     def stop(self):
        """Stop the httpd"""
        logger.debug("Stopping temp webserver")
        self.port.stopListening()
00372     def render_GET(self, request):
        """Handle incoming web requests"""
        logger.debug("Incoming temp webserver hit received")
        nonce = request.args.get("nonce", [None])[0]
        token = request.args.get("oauth_token", [None])[0]
        url = request.args.get("return", ["https://ubuntuone.com/"])[0]
        if nonce and (str(nonce) == str(self.nonce)):
            self.retrieve_function(self.store_yes_no)
            reactor.callLater(3, self.stop)
            return """<!doctype html>
        <html><head><meta http-equiv="refresh"
        content="0;url=%(url)s">
        </head>
        <body>
        <p>You should now automatically <a
        href="%(url)s">return to %(url)s</a>.</p>
        </body>
        </html>
        """ % { 'url' : url }
        else:
            request.setResponseCode(400)
            return """<!doctype html>
        <html><head><title>Error</title></head>
        <body>
        <h1>There was an error</h1>
        <p>The authentication process has not succeeded. This may be a
        temporary problem; please try again in a few minutes.</p>
        </body>
        </html>
        """




Generated by  Doxygen 1.6.0   Back to index