From b83967809da9875dc48d98a9e1291e5873966c48 Mon Sep 17 00:00:00 2001 From: Jay Lee Date: Mon, 19 Jun 2017 14:49:28 -0400 Subject: [PATCH] oauth2client update --- src/oauth2client/__init__.py | 2 +- src/oauth2client/_helpers.py | 4 +- src/oauth2client/_pkce.py | 8 +- src/oauth2client/client.py | 23 +- src/oauth2client/contrib/_fcntl_opener.py | 81 --- src/oauth2client/contrib/_metadata.py | 6 +- src/oauth2client/contrib/_win32_opener.py | 106 ---- src/oauth2client/contrib/devshell.py | 1 + .../contrib/django_util/models.py | 11 +- src/oauth2client/contrib/flask_util.py | 2 + src/oauth2client/contrib/locked_file.py | 234 -------- src/oauth2client/contrib/multistore_file.py | 505 ------------------ src/oauth2client/tools.py | 11 +- src/oauth2client/util.py | 206 ------- 14 files changed, 41 insertions(+), 1159 deletions(-) delete mode 100644 src/oauth2client/contrib/_fcntl_opener.py delete mode 100644 src/oauth2client/contrib/_win32_opener.py delete mode 100644 src/oauth2client/contrib/locked_file.py delete mode 100644 src/oauth2client/contrib/multistore_file.py delete mode 100644 src/oauth2client/util.py diff --git a/src/oauth2client/__init__.py b/src/oauth2client/__init__.py index ef19360a..30edb185 100644 --- a/src/oauth2client/__init__.py +++ b/src/oauth2client/__init__.py @@ -14,7 +14,7 @@ """Client library for using OAuth2, especially with Google APIs.""" -__version__ = '4.0.0' +__version__ = '4.1.0' GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth' GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code' diff --git a/src/oauth2client/_helpers.py b/src/oauth2client/_helpers.py index d9cda749..e9123971 100644 --- a/src/oauth2client/_helpers.py +++ b/src/oauth2client/_helpers.py @@ -251,8 +251,8 @@ def validate_file(filename): raise IOError(_SYM_LINK_MESSAGE.format(filename)) elif os.path.isdir(filename): raise IOError(_IS_DIR_MESSAGE.format(filename)) - #elif not os.path.isfile(filename): - # warnings.warn(_MISSING_FILE_MESSAGE.format(filename)) + elif not os.path.isfile(filename): + warnings.warn(_MISSING_FILE_MESSAGE.format(filename)) def _parse_pem_key(raw_key_input): diff --git a/src/oauth2client/_pkce.py b/src/oauth2client/_pkce.py index 8f22f579..e4952d8c 100644 --- a/src/oauth2client/_pkce.py +++ b/src/oauth2client/_pkce.py @@ -38,7 +38,7 @@ def code_verifier(n_bytes=64): Returns: Bytestring, representing urlsafe base64-encoded random data. """ - verifier = base64.urlsafe_b64encode(os.urandom(n_bytes)) + verifier = base64.urlsafe_b64encode(os.urandom(n_bytes)).rstrip(b'=') # https://tools.ietf.org/html/rfc7636#section-4.1 # minimum length of 43 characters and a maximum length of 128 characters. if len(verifier) < 43: @@ -60,6 +60,8 @@ def code_challenge(verifier): code_verifier(). Returns: - Bytestring, representing a urlsafe base64-encoded sha256 hash digest. + Bytestring, representing a urlsafe base64-encoded sha256 hash digest, + without '=' padding. """ - return base64.urlsafe_b64encode(hashlib.sha256(verifier).digest()) + digest = hashlib.sha256(verifier).digest() + return base64.urlsafe_b64encode(digest).rstrip(b'=') diff --git a/src/oauth2client/client.py b/src/oauth2client/client.py index fbcd70c3..27d62eb1 100644 --- a/src/oauth2client/client.py +++ b/src/oauth2client/client.py @@ -108,7 +108,7 @@ except ValueError: # pragma: NO COVER GCE_METADATA_TIMEOUT = 3 _SERVER_SOFTWARE = 'SERVER_SOFTWARE' -_GCE_METADATA_URI = 'http://169.254.169.254' +_GCE_METADATA_URI = 'http://' + os.getenv('GCE_METADATA_IP', '169.254.169.254') _METADATA_FLAVOR_HEADER = 'metadata-flavor' # lowercase header _DESIRED_METADATA_FLAVOR = 'Google' _GCE_HEADERS = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR} @@ -271,7 +271,7 @@ class Credentials(object): to_serialize[key] = val.decode('utf-8') if isinstance(val, set): to_serialize[key] = list(val) - return json.dumps(to_serialize, indent=4, sort_keys=True) + return json.dumps(to_serialize) def to_json(self): """Creating a JSON representation of an instance of Credentials. @@ -451,7 +451,7 @@ class OAuth2Credentials(Credentials): def __init__(self, access_token, client_id, client_secret, refresh_token, token_expiry, token_uri, user_agent, revoke_uri=None, id_token=None, token_response=None, scopes=None, - token_info_uri=None): + token_info_uri=None, id_token_jwt=None): """Create an instance of OAuth2Credentials. This constructor is not usually called by the user, instead @@ -474,8 +474,11 @@ class OAuth2Credentials(Credentials): because some providers (e.g. wordpress.com) include extra fields that clients may want. scopes: list, authorized scopes for these credentials. - token_info_uri: string, the URI for the token info endpoint. Defaults - to None; scopes can not be refreshed if this is None. + token_info_uri: string, the URI for the token info endpoint. + Defaults to None; scopes can not be refreshed if + this is None. + id_token_jwt: string, the encoded and signed identity JWT. The + decoded version of this is stored in id_token. Notes: store: callable, A callable that when passed a Credential @@ -493,6 +496,7 @@ class OAuth2Credentials(Credentials): self.user_agent = user_agent self.revoke_uri = revoke_uri self.id_token = id_token + self.id_token_jwt = id_token_jwt self.token_response = token_response self.scopes = set(_helpers.string_to_scopes(scopes or [])) self.token_info_uri = token_info_uri @@ -621,6 +625,7 @@ class OAuth2Credentials(Credentials): data['user_agent'], revoke_uri=data.get('revoke_uri', None), id_token=data.get('id_token', None), + id_token_jwt=data.get('id_token_jwt', None), token_response=data.get('token_response', None), scopes=data.get('scopes', None), token_info_uri=data.get('token_info_uri', None)) @@ -786,8 +791,10 @@ class OAuth2Credentials(Credentials): self.token_expiry = None if 'id_token' in d: self.id_token = _extract_id_token(d['id_token']) + self.id_token_jwt = d['id_token'] else: self.id_token = None + self.id_token_jwt = None # On temporary refresh errors, the user does not actually have to # re-authorize, so we unflag here. self.invalid = False @@ -1771,7 +1778,7 @@ class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', ( def _oauth2_web_server_flow_params(kwargs): """Configures redirect URI parameters for OAuth2WebServerFlow.""" params = { -# 'access_type': 'offline', + 'access_type': 'offline', 'response_type': 'code', } @@ -2059,15 +2066,17 @@ class OAuth2WebServerFlow(Flow): token_expiry = delta + _UTCNOW() extracted_id_token = None + id_token_jwt = None if 'id_token' in d: extracted_id_token = _extract_id_token(d['id_token']) + id_token_jwt = d['id_token'] logger.info('Successfully retrieved access token') return OAuth2Credentials( access_token, self.client_id, self.client_secret, refresh_token, token_expiry, self.token_uri, self.user_agent, revoke_uri=self.revoke_uri, id_token=extracted_id_token, - token_response=d, scopes=self.scope, + id_token_jwt=id_token_jwt, token_response=d, scopes=self.scope, token_info_uri=self.token_info_uri) else: logger.info('Failed to retrieve access token: %s', content) diff --git a/src/oauth2client/contrib/_fcntl_opener.py b/src/oauth2client/contrib/_fcntl_opener.py deleted file mode 100644 index ae6c85b5..00000000 --- a/src/oauth2client/contrib/_fcntl_opener.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# 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. - -import errno -import fcntl -import time - -from oauth2client.contrib import locked_file - - -class _FcntlOpener(locked_file._Opener): - """Open, lock, and unlock a file using fcntl.lockf.""" - - def open_and_lock(self, timeout, delay): - """Open the file and lock it. - - Args: - timeout: float, How long to try to lock for. - delay: float, How long to wait between retries - - Raises: - AlreadyLockedException: if the lock is already acquired. - IOError: if the open fails. - CredentialsFileSymbolicLinkError: if the file is a symbolic - link. - """ - if self._locked: - raise locked_file.AlreadyLockedException( - 'File {0} is already locked'.format(self._filename)) - start_time = time.time() - - locked_file.validate_file(self._filename) - try: - self._fh = open(self._filename, self._mode) - except IOError as e: - # If we can't access with _mode, try _fallback_mode and - # don't lock. - if e.errno in (errno.EPERM, errno.EACCES): - self._fh = open(self._filename, self._fallback_mode) - return - - # We opened in _mode, try to lock the file. - while True: - try: - fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX) - self._locked = True - return - except IOError as e: - # If not retrying, then just pass on the error. - if timeout == 0: - raise - if e.errno != errno.EACCES: - raise - # We could not acquire the lock. Try again. - if (time.time() - start_time) >= timeout: - locked_file.logger.warn('Could not lock %s in %s seconds', - self._filename, timeout) - if self._fh: - self._fh.close() - self._fh = open(self._filename, self._fallback_mode) - return - time.sleep(delay) - - def unlock_and_close(self): - """Close and unlock the file using the fcntl.lockf primitive.""" - if self._locked: - fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN) - self._locked = False - if self._fh: - self._fh.close() diff --git a/src/oauth2client/contrib/_metadata.py b/src/oauth2client/contrib/_metadata.py index 1a18bd43..564cd398 100644 --- a/src/oauth2client/contrib/_metadata.py +++ b/src/oauth2client/contrib/_metadata.py @@ -19,6 +19,7 @@ See https://cloud.google.com/compute/docs/metadata import datetime import json +import os from six.moves import http_client from six.moves.urllib import parse as urlparse @@ -28,7 +29,8 @@ from oauth2client import client from oauth2client import transport -METADATA_ROOT = 'http://metadata.google.internal/computeMetadata/v1/' +METADATA_ROOT = 'http://{}/computeMetadata/v1/'.format( + os.getenv('GCE_METADATA_ROOT', 'metadata.google.internal')) METADATA_HEADERS = {'Metadata-Flavor': 'Google'} @@ -38,7 +40,7 @@ def get(http, path, root=METADATA_ROOT, recursive=None): Args: http: an object to be used to make HTTP requests. path: A string indicating the resource to retrieve. For example, - 'instance/service-accounts/defualt' + 'instance/service-accounts/default' root: A string indicating the full path to the metadata server root. recursive: A boolean indicating whether to do a recursive query of metadata. See diff --git a/src/oauth2client/contrib/_win32_opener.py b/src/oauth2client/contrib/_win32_opener.py deleted file mode 100644 index 34b4f481..00000000 --- a/src/oauth2client/contrib/_win32_opener.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# 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. - -import errno -import time - -import pywintypes -import win32con -import win32file - -from oauth2client.contrib import locked_file - - -class _Win32Opener(locked_file._Opener): - """Open, lock, and unlock a file using windows primitives.""" - - # Error #33: - # 'The process cannot access the file because another process' - FILE_IN_USE_ERROR = 33 - - # Error #158: - # 'The segment is already unlocked.' - FILE_ALREADY_UNLOCKED_ERROR = 158 - - def open_and_lock(self, timeout, delay): - """Open the file and lock it. - - Args: - timeout: float, How long to try to lock for. - delay: float, How long to wait between retries - - Raises: - AlreadyLockedException: if the lock is already acquired. - IOError: if the open fails. - CredentialsFileSymbolicLinkError: if the file is a symbolic - link. - """ - if self._locked: - raise locked_file.AlreadyLockedException( - 'File {0} is already locked'.format(self._filename)) - start_time = time.time() - - locked_file.validate_file(self._filename) - try: - self._fh = open(self._filename, self._mode) - except IOError as e: - # If we can't access with _mode, try _fallback_mode - # and don't lock. - if e.errno == errno.EACCES: - self._fh = open(self._filename, self._fallback_mode) - return - - # We opened in _mode, try to lock the file. - while True: - try: - hfile = win32file._get_osfhandle(self._fh.fileno()) - win32file.LockFileEx( - hfile, - (win32con.LOCKFILE_FAIL_IMMEDIATELY | - win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000, - pywintypes.OVERLAPPED()) - self._locked = True - return - except pywintypes.error as e: - if timeout == 0: - raise - - # If the error is not that the file is already - # in use, raise. - if e[0] != _Win32Opener.FILE_IN_USE_ERROR: - raise - - # We could not acquire the lock. Try again. - if (time.time() - start_time) >= timeout: - locked_file.logger.warn('Could not lock %s in %s seconds', - self._filename, timeout) - if self._fh: - self._fh.close() - self._fh = open(self._filename, self._fallback_mode) - return - time.sleep(delay) - - def unlock_and_close(self): - """Close and unlock the file using the win32 primitive.""" - if self._locked: - try: - hfile = win32file._get_osfhandle(self._fh.fileno()) - win32file.UnlockFileEx(hfile, 0, -0x10000, - pywintypes.OVERLAPPED()) - except pywintypes.error as e: - if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR: - raise - self._locked = False - if self._fh: - self._fh.close() diff --git a/src/oauth2client/contrib/devshell.py b/src/oauth2client/contrib/devshell.py index c1906eb6..691765f0 100644 --- a/src/oauth2client/contrib/devshell.py +++ b/src/oauth2client/contrib/devshell.py @@ -37,6 +37,7 @@ class CommunicationError(Error): class NoDevshellServer(Error): """Error when no Developer Shell server can be contacted.""" + # The request for credential information to the Developer Shell client socket # is always an empty PBLite-formatted JSON object, so just define it as a # constant. diff --git a/src/oauth2client/contrib/django_util/models.py b/src/oauth2client/contrib/django_util/models.py index 87e1da70..37cc6970 100644 --- a/src/oauth2client/contrib/django_util/models.py +++ b/src/oauth2client/contrib/django_util/models.py @@ -19,6 +19,7 @@ import pickle from django.db import models from django.utils import encoding +import jsonpickle import oauth2client @@ -48,7 +49,12 @@ class CredentialsField(models.Field): elif isinstance(value, oauth2client.client.Credentials): return value else: - return pickle.loads(base64.b64decode(encoding.smart_bytes(value))) + try: + return jsonpickle.decode( + base64.b64decode(encoding.smart_bytes(value)).decode()) + except ValueError: + return pickle.loads( + base64.b64decode(encoding.smart_bytes(value))) def get_prep_value(self, value): """Overrides ``models.Field`` method. This is used to convert @@ -58,7 +64,8 @@ class CredentialsField(models.Field): if value is None: return None else: - return encoding.smart_text(base64.b64encode(pickle.dumps(value))) + return encoding.smart_text( + base64.b64encode(jsonpickle.encode(value).encode())) def value_to_string(self, obj): """Convert the field value from the provided model to a string. diff --git a/src/oauth2client/contrib/flask_util.py b/src/oauth2client/contrib/flask_util.py index 6d7d8f7f..fabd613b 100644 --- a/src/oauth2client/contrib/flask_util.py +++ b/src/oauth2client/contrib/flask_util.py @@ -176,6 +176,7 @@ try: from flask import request from flask import session from flask import url_for + import markupsafe except ImportError: # pragma: NO COVER raise ImportError('The flask utilities require flask 0.9 or newer.') @@ -388,6 +389,7 @@ class UserOAuth2(object): if 'error' in request.args: reason = request.args.get( 'error_description', request.args.get('error', '')) + reason = markupsafe.escape(reason) return ('Authorization failed: {0}'.format(reason), httplib.BAD_REQUEST) diff --git a/src/oauth2client/contrib/locked_file.py b/src/oauth2client/contrib/locked_file.py deleted file mode 100644 index 0d28ebb0..00000000 --- a/src/oauth2client/contrib/locked_file.py +++ /dev/null @@ -1,234 +0,0 @@ -# Copyright 2014 Google Inc. All rights reserved. -# -# 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. - -"""Locked file interface that should work on Unix and Windows pythons. - -This module first tries to use fcntl locking to ensure serialized access -to a file, then falls back on a lock file if that is unavialable. - -Usage:: - - f = LockedFile('filename', 'r+b', 'rb') - f.open_and_lock() - if f.is_locked(): - print('Acquired filename with r+b mode') - f.file_handle().write('locked data') - else: - print('Acquired filename with rb mode') - f.unlock_and_close() - -""" - -from __future__ import print_function - -import errno -import logging -import os -import time - -from oauth2client import util - - -__author__ = 'cache@google.com (David T McWherter)' - -logger = logging.getLogger(__name__) - - -class CredentialsFileSymbolicLinkError(Exception): - """Credentials files must not be symbolic links.""" - - -class AlreadyLockedException(Exception): - """Trying to lock a file that has already been locked by the LockedFile.""" - pass - - -def validate_file(filename): - if os.path.islink(filename): - raise CredentialsFileSymbolicLinkError( - 'File: {0} is a symbolic link.'.format(filename)) - - -class _Opener(object): - """Base class for different locking primitives.""" - - def __init__(self, filename, mode, fallback_mode): - """Create an Opener. - - Args: - filename: string, The pathname of the file. - mode: string, The preferred mode to access the file with. - fallback_mode: string, The mode to use if locking fails. - """ - self._locked = False - self._filename = filename - self._mode = mode - self._fallback_mode = fallback_mode - self._fh = None - self._lock_fd = None - - def is_locked(self): - """Was the file locked.""" - return self._locked - - def file_handle(self): - """The file handle to the file. Valid only after opened.""" - return self._fh - - def filename(self): - """The filename that is being locked.""" - return self._filename - - def open_and_lock(self, timeout, delay): - """Open the file and lock it. - - Args: - timeout: float, How long to try to lock for. - delay: float, How long to wait between retries. - """ - pass - - def unlock_and_close(self): - """Unlock and close the file.""" - pass - - -class _PosixOpener(_Opener): - """Lock files using Posix advisory lock files.""" - - def open_and_lock(self, timeout, delay): - """Open the file and lock it. - - Tries to create a .lock file next to the file we're trying to open. - - Args: - timeout: float, How long to try to lock for. - delay: float, How long to wait between retries. - - Raises: - AlreadyLockedException: if the lock is already acquired. - IOError: if the open fails. - CredentialsFileSymbolicLinkError if the file is a symbolic link. - """ - if self._locked: - raise AlreadyLockedException( - 'File {0} is already locked'.format(self._filename)) - self._locked = False - - validate_file(self._filename) - try: - self._fh = open(self._filename, self._mode) - except IOError as e: - # If we can't access with _mode, try _fallback_mode and don't lock. - if e.errno == errno.EACCES: - self._fh = open(self._filename, self._fallback_mode) - return - - lock_filename = self._posix_lockfile(self._filename) - start_time = time.time() - while True: - try: - self._lock_fd = os.open(lock_filename, - os.O_CREAT | os.O_EXCL | os.O_RDWR) - self._locked = True - break - - except OSError as e: - if e.errno != errno.EEXIST: - raise - if (time.time() - start_time) >= timeout: - logger.warn('Could not acquire lock %s in %s seconds', - lock_filename, timeout) - # Close the file and open in fallback_mode. - if self._fh: - self._fh.close() - self._fh = open(self._filename, self._fallback_mode) - return - time.sleep(delay) - - def unlock_and_close(self): - """Unlock a file by removing the .lock file, and close the handle.""" - if self._locked: - lock_filename = self._posix_lockfile(self._filename) - os.close(self._lock_fd) - os.unlink(lock_filename) - self._locked = False - self._lock_fd = None - if self._fh: - self._fh.close() - - def _posix_lockfile(self, filename): - """The name of the lock file to use for posix locking.""" - return '{0}.lock'.format(filename) - - -class LockedFile(object): - """Represent a file that has exclusive access.""" - - @util.positional(4) - def __init__(self, filename, mode, fallback_mode, use_native_locking=True): - """Construct a LockedFile. - - Args: - filename: string, The path of the file to open. - mode: string, The mode to try to open the file with. - fallback_mode: string, The mode to use if locking fails. - use_native_locking: bool, Whether or not fcntl/win32 locking is - used. - """ - opener = None - if not opener and use_native_locking: - try: - from oauth2client.contrib._win32_opener import _Win32Opener - opener = _Win32Opener(filename, mode, fallback_mode) - except ImportError: - try: - from oauth2client.contrib._fcntl_opener import _FcntlOpener - opener = _FcntlOpener(filename, mode, fallback_mode) - except ImportError: - pass - - if not opener: - opener = _PosixOpener(filename, mode, fallback_mode) - - self._opener = opener - - def filename(self): - """Return the filename we were constructed with.""" - return self._opener._filename - - def file_handle(self): - """Return the file_handle to the opened file.""" - return self._opener.file_handle() - - def is_locked(self): - """Return whether we successfully locked the file.""" - return self._opener.is_locked() - - def open_and_lock(self, timeout=0, delay=0.05): - """Open the file, trying to lock it. - - Args: - timeout: float, The number of seconds to try to acquire the lock. - delay: float, The number of seconds to wait between retry attempts. - - Raises: - AlreadyLockedException: if the lock is already acquired. - IOError: if the open fails. - """ - self._opener.open_and_lock(timeout, delay) - - def unlock_and_close(self): - """Unlock and close a file.""" - self._opener.unlock_and_close() diff --git a/src/oauth2client/contrib/multistore_file.py b/src/oauth2client/contrib/multistore_file.py deleted file mode 100644 index 10f4cb40..00000000 --- a/src/oauth2client/contrib/multistore_file.py +++ /dev/null @@ -1,505 +0,0 @@ -# Copyright 2014 Google Inc. All rights reserved. -# -# 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': '', - 'userAgent': '', - 'scope': '' - }, - 'credential': { - # JSON serialized Credentials. - } - } - ] - } - -""" - -import errno -import json -import logging -import os -import threading - -from oauth2client import client -from oauth2client import util -from oauth2client.contrib import locked_file - -__author__ = 'jbeda@google.com (Joe Beda)' - -logger = logging.getLogger(__name__) - -logger.warning( - 'The oauth2client.contrib.multistore_file module has been deprecated and ' - 'will be removed in the next release of oauth2client. Please migrate to ' - 'multiprocess_file_storage.') - -# A dict from 'filename'->_MultiStore instances -_multistores = {} -_multistores_lock = threading.Lock() - - -class Error(Exception): - """Base error for this module.""" - - -class NewerCredentialStoreError(Error): - """The credential store is a newer version than supported.""" - - -def _dict_to_tuple_key(dictionary): - """Converts a dictionary to a tuple that can be used as an immutable key. - - The resulting key is always sorted so that logically equivalent - dictionaries always produce an identical tuple for a key. - - Args: - dictionary: the dictionary to use as the key. - - Returns: - A tuple representing the dictionary in it's naturally sorted ordering. - """ - return tuple(sorted(dictionary.items())) - - -@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 = _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 = locked_file.LockedFile(filename, 'r+', 'r') - 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(client.Storage): - """A Storage object that can 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(0o177) - try: - open(self._file.filename(), 'a+b').close() - finally: - os.umask(old_umask) - - def _lock(self): - """Lock the entire multistore.""" - self._thread_lock.acquire() - try: - self._file.open_and_lock() - except (IOError, OSError) as e: - if e.errno == errno.ENOSYS: - logger.warn('File system does not support locking the ' - 'credentials file.') - elif e.errno == errno.ENOLCK: - logger.warn('File system is out of resources for writing the ' - 'credentials file (is your disk full?).') - elif e.errno == errno.EDEADLK: - logger.warn('Lock contention on multistore file, opening ' - 'in read-only mode.') - elif e.errno == errno.EACCES: - logger.warn('Cannot access credentials file.') - else: - raise - 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, separators=(',', ': ')) - 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 {0}. ' - 'Only file_version of 1 is supported.'.format(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 = _dict_to_tuple_key(raw_key) - credential = None - credential = client.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) diff --git a/src/oauth2client/tools.py b/src/oauth2client/tools.py index f15dfc12..51669934 100644 --- a/src/oauth2client/tools.py +++ b/src/oauth2client/tools.py @@ -92,6 +92,7 @@ def _CreateArgumentParser(): help='Set the logging level of detail.') return parser + # argparser is an ArgumentParser that contains command-line options expected # by tools.run(). Pass it in as part of the 'parents' argument to your own # ArgumentParser. @@ -217,16 +218,6 @@ def run_flow(flow, storage, flags=None, http=None): flow.redirect_uri = oauth_callback authorize_url = flow.step1_get_authorize_url() - if flags.short_url: - try: - from googleapiclient.discovery import build - service = build('urlshortener', 'v1', http=http) - url_result = service.url().insert(body={'longUrl': authorize_url}, - key=u'AIzaSyBlmgbii8QfJSYmC9VTMOfqrAt5Vj5wtzE').execute() - authorize_url = url_result['id'] - except: - pass - if not flags.noauth_local_webserver: import webbrowser webbrowser.open(authorize_url, new=1, autoraise=True) diff --git a/src/oauth2client/util.py b/src/oauth2client/util.py deleted file mode 100644 index e3ba62b5..00000000 --- a/src/oauth2client/util.py +++ /dev/null @@ -1,206 +0,0 @@ -# Copyright 2014 Google Inc. All rights reserved. -# -# 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. - -"""Common utility library.""" - -import functools -import inspect -import logging - -import six -from six.moves import urllib - - -__author__ = [ - 'rafek@google.com (Rafe Kaplan)', - 'guido@google.com (Guido van Rossum)', -] - -__all__ = [ - 'positional', - 'POSITIONAL_WARNING', - 'POSITIONAL_EXCEPTION', - 'POSITIONAL_IGNORE', -] - -logger = logging.getLogger(__name__) - -POSITIONAL_WARNING = 'WARNING' -POSITIONAL_EXCEPTION = 'EXCEPTION' -POSITIONAL_IGNORE = 'IGNORE' -POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION, - POSITIONAL_IGNORE]) - -positional_parameters_enforcement = POSITIONAL_WARNING - - -def positional(max_positional_args): - """A decorator to declare that only the first N arguments my be positional. - - This decorator makes it easy to support Python 3 style keyword-only - parameters. For example, in Python 3 it is possible to write:: - - def fn(pos1, *, kwonly1=None, kwonly1=None): - ... - - All named parameters after ``*`` must be a keyword:: - - fn(10, 'kw1', 'kw2') # Raises exception. - fn(10, kwonly1='kw1') # Ok. - - Example - ^^^^^^^ - - To define a function like above, do:: - - @positional(1) - def fn(pos1, kwonly1=None, kwonly2=None): - ... - - If no default value is provided to a keyword argument, it becomes a - required keyword argument:: - - @positional(0) - def fn(required_kw): - ... - - This must be called with the keyword parameter:: - - fn() # Raises exception. - fn(10) # Raises exception. - fn(required_kw=10) # Ok. - - When defining instance or class methods always remember to account for - ``self`` and ``cls``:: - - class MyClass(object): - - @positional(2) - def my_method(self, pos1, kwonly1=None): - ... - - @classmethod - @positional(2) - def my_method(cls, pos1, kwonly1=None): - ... - - The positional decorator behavior is controlled by - ``util.positional_parameters_enforcement``, which may be set to - ``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or - ``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do - nothing, respectively, if a declaration is violated. - - Args: - max_positional_arguments: Maximum number of positional arguments. All - parameters after the this index must be - keyword only. - - Returns: - A decorator that prevents using arguments after max_positional_args - from being used as positional parameters. - - Raises: - TypeError: if a key-word only argument is provided as a positional - parameter, but only if - util.positional_parameters_enforcement is set to - POSITIONAL_EXCEPTION. - """ - - def positional_decorator(wrapped): - @functools.wraps(wrapped) - def positional_wrapper(*args, **kwargs): - if len(args) > max_positional_args: - plural_s = '' - if max_positional_args != 1: - plural_s = 's' - message = ('{function}() takes at most {args_max} positional ' - 'argument{plural} ({args_given} given)'.format( - function=wrapped.__name__, - args_max=max_positional_args, - args_given=len(args), - plural=plural_s)) - if positional_parameters_enforcement == POSITIONAL_EXCEPTION: - raise TypeError(message) - elif positional_parameters_enforcement == POSITIONAL_WARNING: - logger.warning(message) - return wrapped(*args, **kwargs) - return positional_wrapper - - if isinstance(max_positional_args, six.integer_types): - return positional_decorator - else: - args, _, _, defaults = inspect.getargspec(max_positional_args) - return positional(len(args) - len(defaults))(max_positional_args) - - -def scopes_to_string(scopes): - """Converts scope value to a string. - - If scopes is a string then it is simply passed through. If scopes is an - iterable then a string is returned that is all the individual scopes - concatenated with spaces. - - Args: - scopes: string or iterable of strings, the scopes. - - Returns: - The scopes formatted as a single string. - """ - if isinstance(scopes, six.string_types): - return scopes - else: - return ' '.join(scopes) - - -def string_to_scopes(scopes): - """Converts stringifed scope value to a list. - - If scopes is a list then it is simply passed through. If scopes is an - string then a list of each individual scope is returned. - - Args: - scopes: a string or iterable of strings, the scopes. - - Returns: - The scopes in a list. - """ - if not scopes: - return [] - if isinstance(scopes, six.string_types): - return scopes.split(' ') - else: - return scopes - - -def _add_query_parameter(url, name, value): - """Adds a query parameter to a url. - - Replaces the current value if it already exists in the URL. - - Args: - url: string, url to add the query parameter to. - name: string, query parameter name. - value: string, query parameter value. - - Returns: - Updated query parameter. Does not update the url if value is None. - """ - if value is None: - return url - else: - parsed = list(urllib.parse.urlparse(url)) - q = dict(urllib.parse.parse_qsl(parsed[4])) - q[name] = value - parsed[4] = urllib.parse.urlencode(q) - return urllib.parse.urlunparse(parsed)