* Set the minimum version of python to be 2.6, since we don't need to support anything older anymore. * As the first of a series of related cleanups, drop our custom json module (since json is in the stdlib since 2.6).
		
			
				
	
	
		
			466 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			466 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# Copyright 2011 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.
 | 
						|
 | 
						|
"""Multi-credential file store with lock support.
 | 
						|
 | 
						|
This module implements a JSON credential store where multiple
 | 
						|
credentials can be stored in one file. That file supports locking
 | 
						|
both in a single process and across processes.
 | 
						|
 | 
						|
The credential themselves are keyed off of:
 | 
						|
* client_id
 | 
						|
* user_agent
 | 
						|
* scope
 | 
						|
 | 
						|
The format of the stored data is like so:
 | 
						|
{
 | 
						|
  'file_version': 1,
 | 
						|
  'data': [
 | 
						|
    {
 | 
						|
      'key': {
 | 
						|
        'clientId': '<client id>',
 | 
						|
        'userAgent': '<user agent>',
 | 
						|
        'scope': '<scope>'
 | 
						|
      },
 | 
						|
      'credential': {
 | 
						|
        # JSON serialized Credentials.
 | 
						|
      }
 | 
						|
    }
 | 
						|
  ]
 | 
						|
}
 | 
						|
"""
 | 
						|
 | 
						|
__author__ = 'jbeda@google.com (Joe Beda)'
 | 
						|
 | 
						|
import base64
 | 
						|
import errno
 | 
						|
import json
 | 
						|
import logging
 | 
						|
import os
 | 
						|
import threading
 | 
						|
 | 
						|
from oauth2client.client import Storage as BaseStorage
 | 
						|
from oauth2client.client import Credentials
 | 
						|
from oauth2client import util
 | 
						|
from locked_file import LockedFile
 | 
						|
 | 
						|
logger = logging.getLogger(__name__)
 | 
						|
 | 
						|
# A dict from 'filename'->_MultiStore instances
 | 
						|
_multistores = {}
 | 
						|
_multistores_lock = threading.Lock()
 | 
						|
 | 
						|
 | 
						|
class Error(Exception):
 | 
						|
  """Base error for this module."""
 | 
						|
  pass
 | 
						|
 | 
						|
 | 
						|
class NewerCredentialStoreError(Error):
 | 
						|
  """The credential store is a newer version that supported."""
 | 
						|
  pass
 | 
						|
 | 
						|
 | 
						|
@util.positional(4)
 | 
						|
def get_credential_storage(filename, client_id, user_agent, scope,
 | 
						|
                           warn_on_readonly=True):
 | 
						|
  """Get a Storage instance for a credential.
 | 
						|
 | 
						|
  Args:
 | 
						|
    filename: The JSON file storing a set of credentials
 | 
						|
    client_id: The client_id for the credential
 | 
						|
    user_agent: The user agent for the credential
 | 
						|
    scope: string or iterable of strings, Scope(s) being requested
 | 
						|
    warn_on_readonly: if True, log a warning if the store is readonly
 | 
						|
 | 
						|
  Returns:
 | 
						|
    An object derived from client.Storage for getting/setting the
 | 
						|
    credential.
 | 
						|
  """
 | 
						|
  # Recreate the legacy key with these specific parameters
 | 
						|
  key = {'clientId': client_id, 'userAgent': user_agent,
 | 
						|
         'scope': util.scopes_to_string(scope)}
 | 
						|
  return get_credential_storage_custom_key(
 | 
						|
      filename, key, warn_on_readonly=warn_on_readonly)
 | 
						|
 | 
						|
 | 
						|
@util.positional(2)
 | 
						|
def get_credential_storage_custom_string_key(
 | 
						|
    filename, key_string, warn_on_readonly=True):
 | 
						|
  """Get a Storage instance for a credential using a single string as a key.
 | 
						|
 | 
						|
  Allows you to provide a string as a custom key that will be used for
 | 
						|
  credential storage and retrieval.
 | 
						|
 | 
						|
  Args:
 | 
						|
    filename: The JSON file storing a set of credentials
 | 
						|
    key_string: A string to use as the key for storing this credential.
 | 
						|
    warn_on_readonly: if True, log a warning if the store is readonly
 | 
						|
 | 
						|
  Returns:
 | 
						|
    An object derived from client.Storage for getting/setting the
 | 
						|
    credential.
 | 
						|
  """
 | 
						|
  # Create a key dictionary that can be used
 | 
						|
  key_dict = {'key': key_string}
 | 
						|
  return get_credential_storage_custom_key(
 | 
						|
      filename, key_dict, warn_on_readonly=warn_on_readonly)
 | 
						|
 | 
						|
 | 
						|
@util.positional(2)
 | 
						|
def get_credential_storage_custom_key(
 | 
						|
    filename, key_dict, warn_on_readonly=True):
 | 
						|
  """Get a Storage instance for a credential using a dictionary as a key.
 | 
						|
 | 
						|
  Allows you to provide a dictionary as a custom key that will be used for
 | 
						|
  credential storage and retrieval.
 | 
						|
 | 
						|
  Args:
 | 
						|
    filename: The JSON file storing a set of credentials
 | 
						|
    key_dict: A dictionary to use as the key for storing this credential. There
 | 
						|
      is no ordering of the keys in the dictionary. Logically equivalent
 | 
						|
      dictionaries will produce equivalent storage keys.
 | 
						|
    warn_on_readonly: if True, log a warning if the store is readonly
 | 
						|
 | 
						|
  Returns:
 | 
						|
    An object derived from client.Storage for getting/setting the
 | 
						|
    credential.
 | 
						|
  """
 | 
						|
  multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
 | 
						|
  key = util.dict_to_tuple_key(key_dict)
 | 
						|
  return multistore._get_storage(key)
 | 
						|
 | 
						|
 | 
						|
@util.positional(1)
 | 
						|
def get_all_credential_keys(filename, warn_on_readonly=True):
 | 
						|
  """Gets all the registered credential keys in the given Multistore.
 | 
						|
 | 
						|
  Args:
 | 
						|
    filename: The JSON file storing a set of credentials
 | 
						|
    warn_on_readonly: if True, log a warning if the store is readonly
 | 
						|
 | 
						|
  Returns:
 | 
						|
    A list of the credential keys present in the file.  They are returned as
 | 
						|
    dictionaries that can be passed into get_credential_storage_custom_key to
 | 
						|
    get the actual credentials.
 | 
						|
  """
 | 
						|
  multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
 | 
						|
  multistore._lock()
 | 
						|
  try:
 | 
						|
    return multistore._get_all_credential_keys()
 | 
						|
  finally:
 | 
						|
    multistore._unlock()
 | 
						|
 | 
						|
 | 
						|
@util.positional(1)
 | 
						|
def _get_multistore(filename, warn_on_readonly=True):
 | 
						|
  """A helper method to initialize the multistore with proper locking.
 | 
						|
 | 
						|
  Args:
 | 
						|
    filename: The JSON file storing a set of credentials
 | 
						|
    warn_on_readonly: if True, log a warning if the store is readonly
 | 
						|
 | 
						|
  Returns:
 | 
						|
    A multistore object
 | 
						|
  """
 | 
						|
  filename = os.path.expanduser(filename)
 | 
						|
  _multistores_lock.acquire()
 | 
						|
  try:
 | 
						|
    multistore = _multistores.setdefault(
 | 
						|
        filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly))
 | 
						|
  finally:
 | 
						|
    _multistores_lock.release()
 | 
						|
  return multistore
 | 
						|
 | 
						|
 | 
						|
class _MultiStore(object):
 | 
						|
  """A file backed store for multiple credentials."""
 | 
						|
 | 
						|
  @util.positional(2)
 | 
						|
  def __init__(self, filename, warn_on_readonly=True):
 | 
						|
    """Initialize the class.
 | 
						|
 | 
						|
    This will create the file if necessary.
 | 
						|
    """
 | 
						|
    self._file = LockedFile(filename, 'r+b', 'rb')
 | 
						|
    self._thread_lock = threading.Lock()
 | 
						|
    self._read_only = False
 | 
						|
    self._warn_on_readonly = warn_on_readonly
 | 
						|
 | 
						|
    self._create_file_if_needed()
 | 
						|
 | 
						|
    # Cache of deserialized store. This is only valid after the
 | 
						|
    # _MultiStore is locked or _refresh_data_cache is called. This is
 | 
						|
    # of the form of:
 | 
						|
    #
 | 
						|
    # ((key, value), (key, value)...) -> OAuth2Credential
 | 
						|
    #
 | 
						|
    # If this is None, then the store hasn't been read yet.
 | 
						|
    self._data = None
 | 
						|
 | 
						|
  class _Storage(BaseStorage):
 | 
						|
    """A Storage object that knows how to read/write a single credential."""
 | 
						|
 | 
						|
    def __init__(self, multistore, key):
 | 
						|
      self._multistore = multistore
 | 
						|
      self._key = key
 | 
						|
 | 
						|
    def acquire_lock(self):
 | 
						|
      """Acquires any lock necessary to access this Storage.
 | 
						|
 | 
						|
      This lock is not reentrant.
 | 
						|
      """
 | 
						|
      self._multistore._lock()
 | 
						|
 | 
						|
    def release_lock(self):
 | 
						|
      """Release the Storage lock.
 | 
						|
 | 
						|
      Trying to release a lock that isn't held will result in a
 | 
						|
      RuntimeError.
 | 
						|
      """
 | 
						|
      self._multistore._unlock()
 | 
						|
 | 
						|
    def locked_get(self):
 | 
						|
      """Retrieve credential.
 | 
						|
 | 
						|
      The Storage lock must be held when this is called.
 | 
						|
 | 
						|
      Returns:
 | 
						|
        oauth2client.client.Credentials
 | 
						|
      """
 | 
						|
      credential = self._multistore._get_credential(self._key)
 | 
						|
      if credential:
 | 
						|
        credential.set_store(self)
 | 
						|
      return credential
 | 
						|
 | 
						|
    def locked_put(self, credentials):
 | 
						|
      """Write a credential.
 | 
						|
 | 
						|
      The Storage lock must be held when this is called.
 | 
						|
 | 
						|
      Args:
 | 
						|
        credentials: Credentials, the credentials to store.
 | 
						|
      """
 | 
						|
      self._multistore._update_credential(self._key, credentials)
 | 
						|
 | 
						|
    def locked_delete(self):
 | 
						|
      """Delete a credential.
 | 
						|
 | 
						|
      The Storage lock must be held when this is called.
 | 
						|
 | 
						|
      Args:
 | 
						|
        credentials: Credentials, the credentials to store.
 | 
						|
      """
 | 
						|
      self._multistore._delete_credential(self._key)
 | 
						|
 | 
						|
  def _create_file_if_needed(self):
 | 
						|
    """Create an empty file if necessary.
 | 
						|
 | 
						|
    This method will not initialize the file. Instead it implements a
 | 
						|
    simple version of "touch" to ensure the file has been created.
 | 
						|
    """
 | 
						|
    if not os.path.exists(self._file.filename()):
 | 
						|
      old_umask = os.umask(0177)
 | 
						|
      try:
 | 
						|
        open(self._file.filename(), 'a+b').close()
 | 
						|
      finally:
 | 
						|
        os.umask(old_umask)
 | 
						|
 | 
						|
  def _lock(self):
 | 
						|
    """Lock the entire multistore."""
 | 
						|
    self._thread_lock.acquire()
 | 
						|
    self._file.open_and_lock()
 | 
						|
    if not self._file.is_locked():
 | 
						|
      self._read_only = True
 | 
						|
      if self._warn_on_readonly:
 | 
						|
        logger.warn('The credentials file (%s) is not writable. Opening in '
 | 
						|
                    'read-only mode. Any refreshed credentials will only be '
 | 
						|
                    'valid for this run.' % self._file.filename())
 | 
						|
    if os.path.getsize(self._file.filename()) == 0:
 | 
						|
      logger.debug('Initializing empty multistore file')
 | 
						|
      # The multistore is empty so write out an empty file.
 | 
						|
      self._data = {}
 | 
						|
      self._write()
 | 
						|
    elif not self._read_only or self._data is None:
 | 
						|
      # Only refresh the data if we are read/write or we haven't
 | 
						|
      # cached the data yet. If we are readonly, we assume is isn't
 | 
						|
      # changing out from under us and that we only have to read it
 | 
						|
      # once. This prevents us from whacking any new access keys that
 | 
						|
      # we have cached in memory but were unable to write out.
 | 
						|
      self._refresh_data_cache()
 | 
						|
 | 
						|
  def _unlock(self):
 | 
						|
    """Release the lock on the multistore."""
 | 
						|
    self._file.unlock_and_close()
 | 
						|
    self._thread_lock.release()
 | 
						|
 | 
						|
  def _locked_json_read(self):
 | 
						|
    """Get the raw content of the multistore file.
 | 
						|
 | 
						|
    The multistore must be locked when this is called.
 | 
						|
 | 
						|
    Returns:
 | 
						|
      The contents of the multistore decoded as JSON.
 | 
						|
    """
 | 
						|
    assert self._thread_lock.locked()
 | 
						|
    self._file.file_handle().seek(0)
 | 
						|
    return json.load(self._file.file_handle())
 | 
						|
 | 
						|
  def _locked_json_write(self, data):
 | 
						|
    """Write a JSON serializable data structure to the multistore.
 | 
						|
 | 
						|
    The multistore must be locked when this is called.
 | 
						|
 | 
						|
    Args:
 | 
						|
      data: The data to be serialized and written.
 | 
						|
    """
 | 
						|
    assert self._thread_lock.locked()
 | 
						|
    if self._read_only:
 | 
						|
      return
 | 
						|
    self._file.file_handle().seek(0)
 | 
						|
    json.dump(data, self._file.file_handle(), sort_keys=True, indent=2)
 | 
						|
    self._file.file_handle().truncate()
 | 
						|
 | 
						|
  def _refresh_data_cache(self):
 | 
						|
    """Refresh the contents of the multistore.
 | 
						|
 | 
						|
    The multistore must be locked when this is called.
 | 
						|
 | 
						|
    Raises:
 | 
						|
      NewerCredentialStoreError: Raised when a newer client has written the
 | 
						|
        store.
 | 
						|
    """
 | 
						|
    self._data = {}
 | 
						|
    try:
 | 
						|
      raw_data = self._locked_json_read()
 | 
						|
    except Exception:
 | 
						|
      logger.warn('Credential data store could not be loaded. '
 | 
						|
                  'Will ignore and overwrite.')
 | 
						|
      return
 | 
						|
 | 
						|
    version = 0
 | 
						|
    try:
 | 
						|
      version = raw_data['file_version']
 | 
						|
    except Exception:
 | 
						|
      logger.warn('Missing version for credential data store. It may be '
 | 
						|
                  'corrupt or an old version. Overwriting.')
 | 
						|
    if version > 1:
 | 
						|
      raise NewerCredentialStoreError(
 | 
						|
          'Credential file has file_version of %d. '
 | 
						|
          'Only file_version of 1 is supported.' % version)
 | 
						|
 | 
						|
    credentials = []
 | 
						|
    try:
 | 
						|
      credentials = raw_data['data']
 | 
						|
    except (TypeError, KeyError):
 | 
						|
      pass
 | 
						|
 | 
						|
    for cred_entry in credentials:
 | 
						|
      try:
 | 
						|
        (key, credential) = self._decode_credential_from_json(cred_entry)
 | 
						|
        self._data[key] = credential
 | 
						|
      except:
 | 
						|
        # If something goes wrong loading a credential, just ignore it
 | 
						|
        logger.info('Error decoding credential, skipping', exc_info=True)
 | 
						|
 | 
						|
  def _decode_credential_from_json(self, cred_entry):
 | 
						|
    """Load a credential from our JSON serialization.
 | 
						|
 | 
						|
    Args:
 | 
						|
      cred_entry: A dict entry from the data member of our format
 | 
						|
 | 
						|
    Returns:
 | 
						|
      (key, cred) where the key is the key tuple and the cred is the
 | 
						|
        OAuth2Credential object.
 | 
						|
    """
 | 
						|
    raw_key = cred_entry['key']
 | 
						|
    key = util.dict_to_tuple_key(raw_key)
 | 
						|
    credential = None
 | 
						|
    credential = Credentials.new_from_json(json.dumps(cred_entry['credential']))
 | 
						|
    return (key, credential)
 | 
						|
 | 
						|
  def _write(self):
 | 
						|
    """Write the cached data back out.
 | 
						|
 | 
						|
    The multistore must be locked.
 | 
						|
    """
 | 
						|
    raw_data = {'file_version': 1}
 | 
						|
    raw_creds = []
 | 
						|
    raw_data['data'] = raw_creds
 | 
						|
    for (cred_key, cred) in self._data.items():
 | 
						|
      raw_key = dict(cred_key)
 | 
						|
      raw_cred = json.loads(cred.to_json())
 | 
						|
      raw_creds.append({'key': raw_key, 'credential': raw_cred})
 | 
						|
    self._locked_json_write(raw_data)
 | 
						|
 | 
						|
  def _get_all_credential_keys(self):
 | 
						|
    """Gets all the registered credential keys in the multistore.
 | 
						|
 | 
						|
    Returns:
 | 
						|
      A list of dictionaries corresponding to all the keys currently registered
 | 
						|
    """
 | 
						|
    return [dict(key) for key in self._data.keys()]
 | 
						|
 | 
						|
  def _get_credential(self, key):
 | 
						|
    """Get a credential from the multistore.
 | 
						|
 | 
						|
    The multistore must be locked.
 | 
						|
 | 
						|
    Args:
 | 
						|
      key: The key used to retrieve the credential
 | 
						|
 | 
						|
    Returns:
 | 
						|
      The credential specified or None if not present
 | 
						|
    """
 | 
						|
    return self._data.get(key, None)
 | 
						|
 | 
						|
  def _update_credential(self, key, cred):
 | 
						|
    """Update a credential and write the multistore.
 | 
						|
 | 
						|
    This must be called when the multistore is locked.
 | 
						|
 | 
						|
    Args:
 | 
						|
      key: The key used to retrieve the credential
 | 
						|
      cred: The OAuth2Credential to update/set
 | 
						|
    """
 | 
						|
    self._data[key] = cred
 | 
						|
    self._write()
 | 
						|
 | 
						|
  def _delete_credential(self, key):
 | 
						|
    """Delete a credential and write the multistore.
 | 
						|
 | 
						|
    This must be called when the multistore is locked.
 | 
						|
 | 
						|
    Args:
 | 
						|
      key: The key used to retrieve the credential
 | 
						|
    """
 | 
						|
    try:
 | 
						|
      del self._data[key]
 | 
						|
    except KeyError:
 | 
						|
      pass
 | 
						|
    self._write()
 | 
						|
 | 
						|
  def _get_storage(self, key):
 | 
						|
    """Get a Storage object to get/set a credential.
 | 
						|
 | 
						|
    This Storage is a 'view' into the multistore.
 | 
						|
 | 
						|
    Args:
 | 
						|
      key: The key used to retrieve the credential
 | 
						|
 | 
						|
    Returns:
 | 
						|
      A Storage object that can be used to get/set this cred
 | 
						|
    """
 | 
						|
    return self._Storage(self, key)
 |