Logo Search packages:      
Sourcecode: ubuntu-sso-client version File versions  Download package

auth.py

# ubuntu_sso.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 dbus
import os
import socket, httplib, urllib

import gnomekeyring
from oauth import oauth
try:
    from ubuntuone.clientdefs import VERSION
    ubuntuone_client_version = VERSION
except ImportError:
    ubuntuone_client_version = "Unknown"
from ubuntu_sso.key_acls import set_all_key_acls

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

from ubuntu_sso.logger import setupLogging
logger = setupLogging("ubuntu_sso.auth")


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

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

# Monkeypatch httplib so that urllib will fail on invalid certificate
# Only patch if we can import ssl to work around fail in 2.5
try:
    import ssl
except ImportError:
    pass
else:
    def _connect_wrapper(self):
        """Override HTTPSConnection.connect to require certificate checks"""
        sock = socket.create_connection((self.host, self.port), self.timeout)
        try:
            if self._tunnel_host:
                self.sock = sock
                self._tunnel()
        except AttributeError:
            pass
        self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
                cert_reqs=ssl.CERT_REQUIRED, 
                ca_certs="/etc/ssl/certs/ca-certificates.crt")
    httplib.HTTPSConnection.connect = _connect_wrapper


00081 class FancyURLOpenerWithRedirectedPOST(urllib.FancyURLopener):
    """FancyURLopener does not redirect postdata when redirecting POSTs"""
    version = "Ubuntu One/Login (%s)" % ubuntuone_client_version
00084     def redirect_internal(self, url, fp, errcode, errmsg, headers, data):
        """Actually perform a redirect"""
        # All the same as the original, except passing data, below
        if 'location' in headers:
            newurl = headers['location']
        elif 'uri' in headers:
            newurl = headers['uri']
        else:
            return
        fp.read()
        fp.close()
        # In case the server sent a relative URL, join with original:
        newurl = urllib.basejoin(self.type + ":" + url, newurl)
        
        # pass data if present when we redirect
        if data:
            return self.open(newurl, data)
        else:
            return self.open(newurl)

00104 class AuthorisationClient(object):
    """OAuth authorisation client."""
00106     def __init__(self, realm, request_token_url, user_authorisation_url,
                 access_token_url, consumer_key, consumer_secret,
                 callback_parent, callback_denied=None,
                 callback_notoken=None, callback_error=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.callback_notoken = callback_notoken
        self.callback_error = callback_error
        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)

00148     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})

00155     def _forward_error_callback(self, error):
        """Forward an error through callback_error()"""
        if self.callback_error:
            self.callback_error(str(error))
        else:
            raise error

00162     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)

00177     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")

00193     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")
        try:
            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)
        except gnomekeyring.DeniedError:
            logger.debug("Permission denied storing token")
        else:
            # 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)

00223     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

00232     def make_token_request(self, oauth_request):
        """Perform the given `OAuthRequest` and return the associated token."""
        
        logger.debug("Making a token request")
        # Note that we monkeypatched httplib above to handle invalid certs
        # Ways this urlopen can fail:
        # bad certificate
        #    raises IOError, e.args[1] == SSLError, e.args[1].errno == 1
        # No such server 
        #    raises IOError, e.args[1] == SSLError, e.args[1].errno == -2
        try:
            opener = FancyURLOpenerWithRedirectedPOST()
            fp = opener.open(oauth_request.http_url, oauth_request.to_postdata())
            data = fp.read()
        except IOError, e:
            self._forward_error_callback(e)
            return
        
        # 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:
            error = Exception(data)
            logger.error("Token was not successfully retrieved: data was '%s'",
               str(error))
            self._forward_error_callback(error)

00263     def open_in_browser(self, url):
        """Open the given URL in the user's web browser."""
        logger.debug("Opening '%s' in the browser", url)
        p = subprocess.Popen(["xdg-open", url], bufsize=4096,
                             stderr=subprocess.PIPE)
        p.wait()
        if p.returncode != 0:
            errors = "".join(p.stderr.readlines())
            if errors != "":
                self._forward_error_callback(IOError(errors))

00274     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',
                                             follow_name_owner_changes=True)
        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")

            def got_error(error):
                """Handler for D-Bus errors when calling state()."""
                if error.get_dbus_name() == \
                        'org.freedesktop.DBus.Error.ServiceUnknown':
                    logger.debug("NetworkManager not available.")
                    self.acquire_access_token(description, store)
                else:
                    logger.error("Error contacting NetworkManager: %s" % \
                                     str(error))


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

00320     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")

00335     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
        # 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_consumer_and_token(
            callback=callback_url,
            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
        oauth_request = oauth.OAuthRequest.from_token_and_callback(
            http_url=self.user_authorisation_url,
            token=self.request_token)
        nodename = os.uname()[1]
        if nodename:
            oauth_request.set_parameter("description", nodename)
        Thread(target=self.open_in_browser, name="authorization",
               args=(oauth_request.to_url(),)).start()

    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://localhost:%s/?nonce=%s" % (
          temphttpdport.getHost().port, nonce)
        site.set_port(temphttpdport)
        logger.debug("Webserver listening on port '%s'", temphttpdport)
        return callback_url

00384     def retrieve_access_token(self, store=False, verifier=None):
        """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.set_parameter("oauth_verifier", verifier)
        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.error("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)

00408     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_notoken is not None:
                    self.callback_notoken()


00427 class TemporaryTwistedWebServer(resource.Resource):
    """A temporary httpd for the oauth process to call back to"""
    isLeaf = True
00430     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
00438     def set_port(self, port):
        """Save the Twisted port object so we can stop it later"""
        self.port = port
00441     def stop(self):
        """Stop the httpd"""
        logger.debug("Stopping temp webserver")
        self.port.stopListening()
00445     def render_GET(self, request):
        """Handle incoming web requests"""
        logger.debug("Incoming temp webserver hit received")
        nonce = request.args.get("nonce", [None])[0]
        url = request.args.get("return", ["https://one.ubuntu.com/"])[0]
        verifier = request.args.get("oauth_verifier", [None])[0]
        logger.debug("Got verifier %s" % verifier)
        if nonce and (str(nonce) == str(self.nonce) and verifier):
            self.retrieve_function(store=self.store_yes_no, verifier=verifier)
            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:
            self.retrieve_function(store=self.store_yes_no, verifier=verifier)
            reactor.callLater(3, self.stop)
            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