PageRenderTime 49ms CodeModel.GetById 5ms app.highlight 28ms RepoModel.GetById 9ms app.codeStats 0ms

/src/googlecl/authentication.py

http://googlecl.googlecode.com/
Python | 280 lines | 228 code | 12 blank | 40 comment | 14 complexity | 22f405bf90a8b3745260d52fe2678c89 MD5 | raw file
  1# Copyright (C) 2010 Google Inc.
  2#
  3# Licensed under the Apache License, Version 2.0 (the "License");
  4# you may not use this file except in compliance with the License.
  5# You may obtain a copy of the License at
  6#
  7#      http://www.apache.org/licenses/LICENSE-2.0
  8#
  9# Unless required by applicable law or agreed to in writing, software
 10# distributed under the License is distributed on an "AS IS" BASIS,
 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12# See the License for the specific language governing permissions and
 13# limitations under the License.
 14"""Handling authentication to Google for all services."""
 15
 16from __future__ import with_statement
 17
 18import logging
 19import os
 20import pickle
 21import stat
 22import googlecl
 23
 24TOKENS_FILENAME_FORMAT = 'access_tok_%s'
 25LOGGER_NAME = __name__
 26LOG = logging.getLogger(LOGGER_NAME)
 27
 28#XXX: Public-facing functions are confusing, clean up.
 29class AuthenticationManager(object):
 30  """Handles OAuth token for a given service."""
 31
 32  def __init__(self, service_name, client, tokens_path=None):
 33    """Initializes instance.
 34
 35    Args:
 36      service_name: Name of the service.
 37      client: Client accessing the service that requires authentication.
 38      tokens_path: Path to tokens file. Default None to use result from
 39          get_data_path()
 40    """
 41    self.service = service_name
 42    self.client = client
 43    if tokens_path:
 44      self.tokens_path = tokens_path
 45    else:
 46      self.tokens_path = googlecl.get_data_path(TOKENS_FILENAME_FORMAT %
 47                                                client.email,
 48                                                create_missing_dir=True)
 49
 50  def check_access_token(self):
 51    """Checks that the client's access token is valid, remove it if not.
 52
 53    Returns:
 54      True if the token is valid, False otherwise. False will be returned
 55      whether or not the token was successfully removed.
 56    """
 57    try:
 58      token_valid = self.client.IsTokenValid()
 59    except AttributeError, err:
 60      # Attribute errors crop up when using different gdata libraries
 61      # but the same token.
 62      token_valid = False
 63      LOG.debug('Caught AttributeError: ' + str(err))
 64    if token_valid:
 65      LOG.debug('Token valid!')
 66      return True
 67    else:
 68      removed = self.remove_access_token()
 69      if removed:
 70        LOG.debug('Removed invalid token')
 71      else:
 72        LOG.debug('Failed to remove invalid token')
 73      return False
 74
 75  def get_display_name(self, hostid):
 76    """Gets standard display name for access request.
 77
 78    Args:
 79      hostid: Working machine's host id, e.g. "username@hostname"
 80
 81    Returns:
 82      Value to use for xoauth display name parameter to avoid worrying users
 83      with vague requests for access.
 84    """
 85    return 'GoogleCL %s' % hostid
 86
 87  def _move_failed_token_file(self):
 88    """Backs up failed tokens file."""
 89    new_path = self.tokens_path + '.failed'
 90    LOG.debug('Moving ' + self.tokens_path + ' to ' + new_path)
 91    if os.path.isfile(new_path):
 92      LOG.debug(new_path + ' already exists. Deleting it.')
 93      try:
 94        os.remove(new_path)
 95      except EnvironmentError, err:
 96        LOG.debug('Cannot remove old failed token file: ' + str(err))
 97    try:
 98      os.rename(self.tokens_path, new_path)
 99    except EnvironmentError, err:
100      LOG.debug('Cannot rename token file to ' + new_path + ': ' + str(err))
101
102  def read_access_token(self):
103    """Tries to read an authorization token from a file.
104
105    Returns:
106      The access token, if it exists. If the access token cannot be read,
107      returns None.
108    """
109    if os.path.exists(self.tokens_path):
110      with open(self.tokens_path, 'rb') as tokens_file:
111        try:
112          tokens_dict = pickle.load(tokens_file)
113        except ImportError:
114          return None
115      try:
116        token = tokens_dict[self.service]
117      except KeyError:
118        return None
119      else:
120        return token
121    else:
122      return None
123
124  def remove_access_token(self):
125    """Removes an auth token.
126
127    Returns:
128      True if the token was removed from the tokens file, False otherwise.
129    """
130    success = False
131    file_invalid = False
132    if os.path.exists(self.tokens_path):
133      with open(self.tokens_path, 'r+') as tokens_file:
134        try:
135          tokens_dict = pickle.load(tokens_file)
136        except ImportError, err:
137          LOG.error(err)
138          LOG.info('You probably have been using different versions of gdata.')
139          self._move_failed_token_file()
140          return False
141        except IndexError, err:
142          LOG.error(err)
143          self._move_failed_token_file()
144          return False
145
146        try:
147          del tokens_dict[self.service]
148        except KeyError:
149          LOG.debug('No token for ' + self.service)
150        else:
151          try:
152            pickle.dump(tokens_dict, tokens_file)
153          except EnvironmentError, err:
154            # IOError (extends enverror) shouldn't happen, but I've seen
155            # IOError Errno 0 pop up on Windows XP with Python 2.5.
156            LOG.error(err)
157            if err.errno == 0:
158              _move_failed_token_file()
159          else:
160            success = True
161    return success
162
163  def retrieve_access_token(self, display_name, browser_object):
164    """Requests a new access token from Google, writes it upon retrieval.
165
166    The token will not be written to file if it was granted for an account
167    other than the one specified by client.email. Instead, a False value will
168    be returned.
169
170    Returns:
171      True if the token was retrieved and written to file. False otherwise.
172    """
173    domain = get_hd_domain(self.client.email)
174    if self.client.RequestAccess(domain, display_name, None, browser_object):
175      authorized_account = self.client.get_email()
176      # Only write the token if it's for the right user.
177      if self.verify_email(self.client.email, authorized_account):
178        # token is saved in client.auth_token for GDClient,
179        # client.current_token for GDataService.
180        self.write_access_token(self.client.auth_token or
181                                self.client.current_token)
182        return True
183      else:
184        LOG.error('You specified account ' + self.client.email +
185                  ' but granted access for ' + authorized_account + '.' +
186                  ' Please log out of ' + authorized_account +
187                  ' and grant access with ' + self.client.email + '.')
188    else:
189      LOG.error('Failed to get valid access token!')
190    return False
191
192  def set_access_token(self):
193    """Reads an access token from file and set it to be used by the client.
194
195    Returns:
196      True if the token was read and set, False otherwise.
197    """
198    try:
199      token = self.read_access_token()
200    except (KeyError, IndexError):
201      LOG.warning('Token file appears to be corrupted. Not using.')
202    else:
203      if token:
204        LOG.debug('Loaded token from file')
205        self.client.SetOAuthToken(token)
206        return True
207      else:
208        LOG.debug('read_access_token evaluated to False')
209    return False
210
211  def verify_email(self, given_account, authorized_account):
212    """Makes sure user didn't clickfest his/her way into a mistake.
213
214    Args:
215      given_account: String of account specified by the user to GoogleCL,
216          probably by options.user. If domain is not included,
217          assumed to be 'gmail.com'
218      authorized_account: Account returned by client.get_email(). Must
219          include domain!
220
221    Returns:
222      True if given_account and authorized_account match, False otherwise.
223    """
224    if authorized_account.find('@') == -1:
225      raise Exception('authorized_account must include domain!')
226    if given_account.find('@') == -1:
227      given_account += '@gmail.com'
228    return given_account == authorized_account
229
230  def write_access_token(self, token):
231    """Writes an authorization token to a file.
232
233    Args:
234      token: Token object to store.
235    """
236    if os.path.exists(self.tokens_path):
237      with open(self.tokens_path, 'rb') as tokens_file:
238        try:
239          tokens_dict = pickle.load(tokens_file)
240        except (KeyError, IndexError), err:
241          LOG.error(err)
242          LOG.error('Failed to load token file (may be corrupted?)')
243          file_invalid = True
244        except ImportError, err:
245          LOG.error(err)
246          LOG.info('You probably have been using different versions of gdata.')
247          file_invalid = True
248        else:
249          file_invalid = False
250      if file_invalid:
251        self._move_failed_token_file()
252        tokens_dict = {}
253    else:
254      tokens_dict = {}
255    tokens_dict[self.service] = token
256    with open(self.tokens_path, 'wb') as tokens_file:
257      # Ensure only the owner of the file has read/write permission
258      os.chmod(self.tokens_path, stat.S_IRUSR | stat.S_IWUSR)
259      pickle.dump(tokens_dict, tokens_file)
260
261
262def get_hd_domain(username, default_domain='default'):
263  """Returns the domain associated with an email address.
264
265  Intended for use with the OAuth hd parameter for Google.
266
267  Args:
268    username: Username to parse.
269    default_domain: Domain to set if '@suchandsuch.huh' is not part of the
270        username. Defaults to 'default' to specify a regular Google account.
271
272  Returns:
273    String of the domain associated with username.
274  """
275  name, at_sign, domain = username.partition('@')
276  # If user specifies gmail.com, it confuses the hd parameter
277  # (thanks, bartosh!)
278  if domain == 'gmail.com' or domain == 'googlemail.com':
279    return 'default'
280  return domain or default_domain