Copyright (C) 2010 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Handling authentication to Google for all services.
from __future__ import with_statement
import logging
import os
import pickle
import stat
import googlecl
TOKENS_FILENAME_FORMAT = 'access_tok_%s'
LOGGER_NAME = __name__
LOG = logging.getLogger(LOGGER_NAME)
XXX: Public-facing functions are confusing, clean up. Handles OAuth token for a given service.
class AuthenticationManager(object):
Initializes instance.
def __init__(self, service_name, client, tokens_path=None):
Args: service_name: Name of the service. client: Client accessing the service that requires authentication. tokens_path: Path to tokens file. Default None to use result from get_data_path()
self.service = service_name
self.client = client
if tokens_path:
self.tokens_path = tokens_path
else:
self.tokens_path = googlecl.get_data_path(TOKENS_FILENAME_FORMAT %
client.email,
create_missing_dir=True)
Checks that the client's access token is valid, remove it if not.
def check_access_token(self):
Returns: True if the token is valid, False otherwise. False will be returned whether or not the token was successfully removed.
try:
token_valid = self.client.IsTokenValid()
except AttributeError, err:
Attribute errors crop up when using different gdata libraries but the same token.
token_valid = False
LOG.debug('Caught AttributeError: ' + str(err))
if token_valid:
LOG.debug('Token valid!')
return True
else:
removed = self.remove_access_token()
if removed:
LOG.debug('Removed invalid token')
else:
LOG.debug('Failed to remove invalid token')
return False
Gets standard display name for access request.
def get_display_name(self, hostid):
Args: hostid: Working machine's host id, e.g. "username@hostname"
Returns: Value to use for xoauth display name parameter to avoid worrying users with vague requests for access.
return 'GoogleCL %s' % hostid
Backs up failed tokens file.
def _move_failed_token_file(self):
new_path = self.tokens_path + '.failed'
LOG.debug('Moving ' + self.tokens_path + ' to ' + new_path)
if os.path.isfile(new_path):
LOG.debug(new_path + ' already exists. Deleting it.')
try:
os.remove(new_path)
except EnvironmentError, err:
LOG.debug('Cannot remove old failed token file: ' + str(err))
try:
os.rename(self.tokens_path, new_path)
except EnvironmentError, err:
LOG.debug('Cannot rename token file to ' + new_path + ': ' + str(err))
Tries to read an authorization token from a file.
def read_access_token(self):
Returns: The access token, if it exists. If the access token cannot be read, returns None.
if os.path.exists(self.tokens_path):
with open(self.tokens_path, 'rb') as tokens_file:
try:
tokens_dict = pickle.load(tokens_file)
except ImportError:
return None
try:
token = tokens_dict[self.service]
except KeyError:
return None
else:
return token
else:
return None
Removes an auth token.
def remove_access_token(self):
Returns: True if the token was removed from the tokens file, False otherwise.
success = False
file_invalid = False
if os.path.exists(self.tokens_path):
with open(self.tokens_path, 'r+') as tokens_file:
try:
tokens_dict = pickle.load(tokens_file)
except ImportError, err:
LOG.error(err)
LOG.info('You probably have been using different versions of gdata.')
self._move_failed_token_file()
return False
except IndexError, err:
LOG.error(err)
self._move_failed_token_file()
return False
try:
del tokens_dict[self.service]
except KeyError:
LOG.debug('No token for ' + self.service)
else:
try:
pickle.dump(tokens_dict, tokens_file)
except EnvironmentError, err:
IOError (extends enverror) shouldn't happen, but I've seen IOError Errno 0 pop up on Windows XP with Python 2.5.
LOG.error(err)
if err.errno == 0:
_move_failed_token_file()
else:
success = True
return success
Requests a new access token from Google, writes it upon retrieval.
def retrieve_access_token(self, display_name, browser_object):
The token will not be written to file if it was granted for an account other than the one specified by client.email. Instead, a False value will be returned.
Returns: True if the token was retrieved and written to file. False otherwise.
domain = get_hd_domain(self.client.email)
if self.client.RequestAccess(domain, display_name, None, browser_object):
authorized_account = self.client.get_email()
Only write the token if it's for the right user.
if self.verify_email(self.client.email, authorized_account):
token is saved in client.auth_token for GDClient, client.current_token for GDataService.
self.write_access_token(self.client.auth_token or
self.client.current_token)
return True
else:
LOG.error('You specified account ' + self.client.email +
' but granted access for ' + authorized_account + '.' +
' Please log out of ' + authorized_account +
' and grant access with ' + self.client.email + '.')
else:
LOG.error('Failed to get valid access token!')
return False
Reads an access token from file and set it to be used by the client.
def set_access_token(self):
Returns: True if the token was read and set, False otherwise.
try:
token = self.read_access_token()
except (KeyError, IndexError):
LOG.warning('Token file appears to be corrupted. Not using.')
else:
if token:
LOG.debug('Loaded token from file')
self.client.SetOAuthToken(token)
return True
else:
LOG.debug('read_access_token evaluated to False')
return False
Makes sure user didn't clickfest his/her way into a mistake.
def verify_email(self, given_account, authorized_account):
Args: given_account: String of account specified by the user to GoogleCL, probably by options.user. If domain is not included, assumed to be 'gmail.com' authorized_account: Account returned by client.get_email(). Must include domain!
Returns: True if given_account and authorized_account match, False otherwise.
if authorized_account.find('@') == -1:
raise Exception('authorized_account must include domain!')
if given_account.find('@') == -1:
given_account += '@gmail.com'
return given_account == authorized_account
Writes an authorization token to a file.
def write_access_token(self, token):
Args: token: Token object to store.
if os.path.exists(self.tokens_path):
with open(self.tokens_path, 'rb') as tokens_file:
try:
tokens_dict = pickle.load(tokens_file)
except (KeyError, IndexError), err:
LOG.error(err)
LOG.error('Failed to load token file (may be corrupted?)')
file_invalid = True
except ImportError, err:
LOG.error(err)
LOG.info('You probably have been using different versions of gdata.')
file_invalid = True
else:
file_invalid = False
if file_invalid:
self._move_failed_token_file()
tokens_dict = {}
else:
tokens_dict = {}
tokens_dict[self.service] = token
with open(self.tokens_path, 'wb') as tokens_file:
Ensure only the owner of the file has read/write permission
os.chmod(self.tokens_path, stat.S_IRUSR | stat.S_IWUSR)
pickle.dump(tokens_dict, tokens_file)
Returns the domain associated with an email address.
def get_hd_domain(username, default_domain='default'):
Intended for use with the OAuth hd parameter for Google.
Args: username: Username to parse. default_domain: Domain to set if '@suchandsuch.huh' is not part of the username. Defaults to 'default' to specify a regular Google account.
Returns: String of the domain associated with username.
name, at_sign, domain = username.partition('@')
If user specifies gmail.com, it confuses the hd parameter (thanks, bartosh!)
if domain == 'gmail.com' or domain == 'googlemail.com':
return 'default'
return domain or default_domain