| # Copyright 2011 Google Inc. All Rights Reserved. | |
| """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__ = '[email protected] (Joe Beda)' | |
| import base64 | |
| import errno | |
| import logging | |
| import os | |
| import threading | |
| from anyjson import simplejson | |
| 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. | |
| """ | |
| 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() | |
| key = util.dict_to_tuple_key(key_dict) | |
| return multistore._get_storage(key) | |
| 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 simplejson.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) | |
| simplejson.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(simplejson.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 = simplejson.loads(cred.to_json()) | |
| raw_creds.append({'key': raw_key, 'credential': raw_cred}) | |
| self._locked_json_write(raw_data) | |
| 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) |