PageRenderTime 41ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/src/googlecl/authentication.py

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