3
# ubuntuone.oauthdesktop.main - main login handling interface
5
# Author: Stuart Langridge <stuart.langridge@canonical.com>
7
# Copyright 2009 Canonical Ltd.
9
# This program is free software: you can redistribute it and/or modify it
10
# under the terms of the GNU General Public License version 3, as published
11
# by the Free Software Foundation.
13
# This program is distributed in the hope that it will be useful, but
14
# WITHOUT ANY WARRANTY; without even the implied warranties of
15
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
16
# PURPOSE. See the GNU General Public License for more details.
18
# You should have received a copy of the GNU General Public License along
19
# with this program. If not, see <http://www.gnu.org/licenses/>.
20
"""OAuth login handler.
22
A command-line utility which accepts requests for OAuth login over D-Bus,
23
handles the OAuth process (including adding the OAuth access token to the
24
gnome keyring), and then alerts the calling app (and others) with a D-Bus
25
signal so they can retrieve the new token.
28
import dbus.service, urlparse, time, gobject
31
from dbus.mainloop.glib import DBusGMainLoop
33
from ubuntuone.oauthdesktop.config import get_config
35
from ubuntuone.oauthdesktop.logger import setupLogging
36
logger = setupLogging("UbuntuOne.OAuthDesktop.main")
38
DBusGMainLoop(set_as_default=True)
40
# Disable the invalid name warning, as we have a lot of DBus style names
41
# pylint: disable-msg=C0103
43
class NoDefaultConfigError(Exception):
44
"""No default section in configuration file"""
47
class BadRealmError(Exception):
48
"""Realm must be a URL"""
52
"""Actually do the work of processing passed parameters"""
53
def __init__(self, dbus_object, use_libnotify=True):
54
"""Initialize the login processor."""
55
logger.debug("Creating a LoginProcessor")
56
self.use_libnotify = use_libnotify
57
if self.use_libnotify and pynotify:
58
logger.debug("Hooking libnotify")
59
pynotify.init("UbuntuOne Login")
62
self.consumer_key = None
63
self.dbus_object = dbus_object
64
logger.debug("Getting configuration")
65
self.config = get_config()
67
def login(self, realm, consumer_key, do_login=True):
68
"""Initiate an OAuth login"""
69
logger.debug("Initiating OAuth login in LoginProcessor")
70
self.realm = str(realm) # because they are dbus.Strings, not str
71
self.consumer_key = str(consumer_key)
73
logger.debug("Obtaining OAuth urls")
74
(request_token_url, user_authorisation_url,
75
access_token_url, consumer_secret) = self.get_config_urls(realm)
76
logger.debug("OAuth URLs are: request='%s', userauth='%s', " +\
77
"access='%s', secret='%s'", request_token_url,
78
user_authorisation_url, access_token_url, consumer_secret)
80
from ubuntuone.oauthdesktop.auth import AuthorisationClient
81
client = AuthorisationClient(self.realm,
83
user_authorisation_url,
84
access_token_url, self.consumer_key,
86
callback_parent=self.got_token,
87
callback_denied=self.got_denial,
88
callback_notoken=self.got_no_token,
89
callback_error=self.got_error,
92
logger.debug("Calling auth.client.ensure_access_token in thread")
93
gobject.timeout_add_seconds(1, client.ensure_access_token)
95
def clear_token(self, realm, consumer_key):
96
"""Remove the currently stored OAuth token from the keyring."""
97
self.realm = str(realm)
98
self.consumer_key = str(consumer_key)
99
(request_token_url, user_authorisation_url,
100
access_token_url, consumer_secret) = self.get_config_urls(self.realm)
101
from ubuntuone.oauthdesktop.auth import AuthorisationClient
102
client = AuthorisationClient(self.realm,
104
user_authorisation_url,
106
self.consumer_key, consumer_secret,
107
callback_parent=self.got_token,
108
callback_denied=self.got_denial,
109
callback_notoken=self.got_no_token,
110
callback_error=self.got_error)
111
gobject.timeout_add_seconds(1, client.clear_token)
113
def error_handler(self, failure):
114
"""Deal with errors returned from auth process"""
115
logger.debug("Error returned from auth process")
116
self.dbus_object.currently_authing = False # not block future requests
118
def get_config_urls(self, realm):
119
"""Look up the URLs to use in the config file"""
120
logger.debug("Fetching config URLs for realm='%s'", realm)
121
if self.config.has_section(realm):
122
logger.debug("Realm '%s' is in config", realm)
123
request_token_url = self.__get_url(realm, "request_token_url")
124
user_authorisation_url = self.__get_url(realm,
125
"user_authorisation_url")
126
access_token_url = self.__get_url(realm, "access_token_url")
127
consumer_secret = self.__get_option(realm, "consumer_secret")
128
elif realm.startswith("http://localhost") and \
129
self.config.has_section("http://localhost"):
130
logger.debug("Realm is localhost and is in config")
131
request_token_url = self.__get_url("http://localhost",
132
"request_token_url", realm)
133
user_authorisation_url = self.__get_url("http://localhost",
134
"user_authorisation_url", realm)
135
access_token_url = self.__get_url("http://localhost",
136
"access_token_url", realm)
137
consumer_secret = self.__get_option("http://localhost",
139
elif self.is_valid_url(realm):
140
logger.debug("Realm '%s' is not in config", realm)
141
request_token_url = self.__get_url("default",
142
"request_token_url", realm)
143
user_authorisation_url = self.__get_url("default",
144
"user_authorisation_url", realm)
145
access_token_url = self.__get_url("default",
146
"access_token_url", realm)
147
consumer_secret = self.__get_option(realm, "consumer_secret")
149
logger.debug("Realm '%s' is a bad realm", realm)
151
return (request_token_url, user_authorisation_url,
152
access_token_url, consumer_secret)
154
def is_valid_url(self, url):
155
"""Simple check for URL validity"""
156
# pylint: disable-msg=W0612
157
scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
158
if scheme and netloc:
163
def got_token(self, access_token):
164
"""Callback function when access token has been retrieved"""
165
logger.debug("Token retrieved, calling NewCredentials function")
166
self.dbus_object.NewCredentials(self.realm, self.consumer_key)
168
def got_denial(self):
169
"""Callback function when request token has been denied"""
170
self.dbus_object.AuthorizationDenied()
172
def got_no_token(self):
173
"""Callback function when access token is not in keyring."""
174
self.dbus_object.NoCredentials()
176
def got_error(self, message):
177
"""Callback function to emit an error message over DBus."""
178
self.dbus_object.OAuthError(message)
180
def __get_url(self, realm, option, actual_realm=None):
181
"""Construct a full URL from realm and a URLpath for that realm in
184
realm_to_use = actual_realm
187
urlstub = self.__get_option(realm, option)
188
return urlparse.urljoin(realm_to_use, urlstub)
190
def __get_option(self, realm, option):
191
"""Return a specific option for that realm in
192
the config file. If the realm does not exist in the config file,
193
fall back to the [default] section."""
194
if self.config.has_section(realm) and \
195
self.config.has_option(realm, option):
196
urlstub = self.config.get(realm, option)
199
# either the realm exists and this url does not, or
200
# the realm doesn't exist; either way, fall back to [default] section
201
urlstub = self.config.get("default", option, None)
202
if urlstub is not None:
205
# this url does not exist in default section either
206
# this shouldn't happen
207
raise NoDefaultConfigError("No default configuration for %s" % option)
210
class Login(dbus.service.Object):
211
"""Object which listens for D-Bus OAuth requests"""
212
def __init__(self, bus_name):
213
"""Initiate the Login object."""
214
dbus.service.Object.__init__(self, object_path="/", bus_name=bus_name)
215
self.processor = LoginProcessor(self)
216
self.currently_authing = False
217
logger.debug("Login D-Bus service starting up")
219
@dbus.service.method(dbus_interface='com.ubuntuone.Authentication',
220
in_signature='ss', out_signature='')
221
def login(self, realm, consumer_key):
222
"""D-Bus method, exported over the bus, to initiate an OAuth login"""
223
logger.debug("login() D-Bus message received with realm='%s', " +
224
"consumer_key='%s'", realm, consumer_key)
225
if self.currently_authing:
226
logger.debug("Currently in the middle of OAuth: rejecting this")
228
self.currently_authing = True
229
self.processor.login(realm, consumer_key)
231
@dbus.service.method(dbus_interface='com.ubuntuone.Authentication',
232
in_signature='ssb', out_signature='')
233
def maybe_login(self, realm, consumer_key, do_login):
235
D-Bus method, exported over the bus, to maybe initiate an OAuth login
237
logger.debug("maybe_login() D-Bus message received with realm='%s', " +
238
"consumer_key='%s'", realm, consumer_key)
239
if self.currently_authing:
240
logger.debug("Currently in the middle of OAuth: rejecting this")
242
self.currently_authing = True
243
self.processor.login(realm, consumer_key, do_login)
245
@dbus.service.method(dbus_interface='com.ubuntuone.Authentication',
246
in_signature='ss', out_signature='')
247
def clear_token(self, realm, consumer_key):
249
D-Bus method, exported over the bus, to clear the existing token.
251
self.processor.clear_token(realm, consumer_key)
253
@dbus.service.signal(dbus_interface='com.ubuntuone.Authentication',
255
def NewCredentials(self, realm, consumer_key):
256
"""Fire D-Bus signal when the user accepts authorization."""
257
logger.debug("Firing the NewCredentials signal")
258
self.currently_authing = False
259
return (self.processor.realm, self.processor.consumer_key)
261
@dbus.service.signal(dbus_interface='com.ubuntuone.Authentication')
262
def AuthorizationDenied(self):
263
"""Fire the signal when the user denies authorization."""
264
self.currently_authing = False
266
@dbus.service.signal(dbus_interface='com.ubuntuone.Authentication')
267
def NoCredentials(self):
268
"""Fired when the user does not have a token in the keyring."""
269
self.currently_authing = False
271
@dbus.service.signal(dbus_interface='com.ubuntuone.Authentication',
273
def OAuthError(self, message):
274
"""Fire the signal when an error needs to be propagated to the user."""
275
self.currently_authing = False
279
"""Start everything"""
280
logger.debug("Starting up at %s", time.asctime())
281
logger.debug("Installing the Twisted glib2reactor")
282
from twisted.internet import glib2reactor # for non-GUI apps
283
glib2reactor.install()
284
from twisted.internet import reactor
286
logger.debug("Creating the D-Bus service")
287
Login(dbus.service.BusName("com.ubuntuone.Authentication",
288
bus=dbus.SessionBus()))
289
# cleverness here to say:
290
# am I already running (bound to this d-bus name)?
291
# if so, send a signal to the already running instance
292
# this means that this app can be started from an x-ubutnuone: URL
293
# to kick off the signin process
294
logger.debug("Starting the reactor mainloop")