mirror of
https://github.com/GAM-team/GAM.git
synced 2026-07-04 04:41:35 +00:00
oauth2client update
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
"""Client library for using OAuth2, especially with Google APIs."""
|
"""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_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth'
|
||||||
GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code'
|
GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code'
|
||||||
|
|||||||
@@ -251,8 +251,8 @@ def validate_file(filename):
|
|||||||
raise IOError(_SYM_LINK_MESSAGE.format(filename))
|
raise IOError(_SYM_LINK_MESSAGE.format(filename))
|
||||||
elif os.path.isdir(filename):
|
elif os.path.isdir(filename):
|
||||||
raise IOError(_IS_DIR_MESSAGE.format(filename))
|
raise IOError(_IS_DIR_MESSAGE.format(filename))
|
||||||
#elif not os.path.isfile(filename):
|
elif not os.path.isfile(filename):
|
||||||
# warnings.warn(_MISSING_FILE_MESSAGE.format(filename))
|
warnings.warn(_MISSING_FILE_MESSAGE.format(filename))
|
||||||
|
|
||||||
|
|
||||||
def _parse_pem_key(raw_key_input):
|
def _parse_pem_key(raw_key_input):
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ def code_verifier(n_bytes=64):
|
|||||||
Returns:
|
Returns:
|
||||||
Bytestring, representing urlsafe base64-encoded random data.
|
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
|
# https://tools.ietf.org/html/rfc7636#section-4.1
|
||||||
# minimum length of 43 characters and a maximum length of 128 characters.
|
# minimum length of 43 characters and a maximum length of 128 characters.
|
||||||
if len(verifier) < 43:
|
if len(verifier) < 43:
|
||||||
@@ -60,6 +60,8 @@ def code_challenge(verifier):
|
|||||||
code_verifier().
|
code_verifier().
|
||||||
|
|
||||||
Returns:
|
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'=')
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ except ValueError: # pragma: NO COVER
|
|||||||
GCE_METADATA_TIMEOUT = 3
|
GCE_METADATA_TIMEOUT = 3
|
||||||
|
|
||||||
_SERVER_SOFTWARE = 'SERVER_SOFTWARE'
|
_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
|
_METADATA_FLAVOR_HEADER = 'metadata-flavor' # lowercase header
|
||||||
_DESIRED_METADATA_FLAVOR = 'Google'
|
_DESIRED_METADATA_FLAVOR = 'Google'
|
||||||
_GCE_HEADERS = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR}
|
_GCE_HEADERS = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR}
|
||||||
@@ -271,7 +271,7 @@ class Credentials(object):
|
|||||||
to_serialize[key] = val.decode('utf-8')
|
to_serialize[key] = val.decode('utf-8')
|
||||||
if isinstance(val, set):
|
if isinstance(val, set):
|
||||||
to_serialize[key] = list(val)
|
to_serialize[key] = list(val)
|
||||||
return json.dumps(to_serialize, indent=4, sort_keys=True)
|
return json.dumps(to_serialize)
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
"""Creating a JSON representation of an instance of Credentials.
|
"""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,
|
def __init__(self, access_token, client_id, client_secret, refresh_token,
|
||||||
token_expiry, token_uri, user_agent, revoke_uri=None,
|
token_expiry, token_uri, user_agent, revoke_uri=None,
|
||||||
id_token=None, token_response=None, scopes=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.
|
"""Create an instance of OAuth2Credentials.
|
||||||
|
|
||||||
This constructor is not usually called by the user, instead
|
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
|
because some providers (e.g. wordpress.com) include
|
||||||
extra fields that clients may want.
|
extra fields that clients may want.
|
||||||
scopes: list, authorized scopes for these credentials.
|
scopes: list, authorized scopes for these credentials.
|
||||||
token_info_uri: string, the URI for the token info endpoint. Defaults
|
token_info_uri: string, the URI for the token info endpoint.
|
||||||
to None; scopes can not be refreshed if this is None.
|
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:
|
Notes:
|
||||||
store: callable, A callable that when passed a Credential
|
store: callable, A callable that when passed a Credential
|
||||||
@@ -493,6 +496,7 @@ class OAuth2Credentials(Credentials):
|
|||||||
self.user_agent = user_agent
|
self.user_agent = user_agent
|
||||||
self.revoke_uri = revoke_uri
|
self.revoke_uri = revoke_uri
|
||||||
self.id_token = id_token
|
self.id_token = id_token
|
||||||
|
self.id_token_jwt = id_token_jwt
|
||||||
self.token_response = token_response
|
self.token_response = token_response
|
||||||
self.scopes = set(_helpers.string_to_scopes(scopes or []))
|
self.scopes = set(_helpers.string_to_scopes(scopes or []))
|
||||||
self.token_info_uri = token_info_uri
|
self.token_info_uri = token_info_uri
|
||||||
@@ -621,6 +625,7 @@ class OAuth2Credentials(Credentials):
|
|||||||
data['user_agent'],
|
data['user_agent'],
|
||||||
revoke_uri=data.get('revoke_uri', None),
|
revoke_uri=data.get('revoke_uri', None),
|
||||||
id_token=data.get('id_token', None),
|
id_token=data.get('id_token', None),
|
||||||
|
id_token_jwt=data.get('id_token_jwt', None),
|
||||||
token_response=data.get('token_response', None),
|
token_response=data.get('token_response', None),
|
||||||
scopes=data.get('scopes', None),
|
scopes=data.get('scopes', None),
|
||||||
token_info_uri=data.get('token_info_uri', None))
|
token_info_uri=data.get('token_info_uri', None))
|
||||||
@@ -786,8 +791,10 @@ class OAuth2Credentials(Credentials):
|
|||||||
self.token_expiry = None
|
self.token_expiry = None
|
||||||
if 'id_token' in d:
|
if 'id_token' in d:
|
||||||
self.id_token = _extract_id_token(d['id_token'])
|
self.id_token = _extract_id_token(d['id_token'])
|
||||||
|
self.id_token_jwt = d['id_token']
|
||||||
else:
|
else:
|
||||||
self.id_token = None
|
self.id_token = None
|
||||||
|
self.id_token_jwt = None
|
||||||
# On temporary refresh errors, the user does not actually have to
|
# On temporary refresh errors, the user does not actually have to
|
||||||
# re-authorize, so we unflag here.
|
# re-authorize, so we unflag here.
|
||||||
self.invalid = False
|
self.invalid = False
|
||||||
@@ -1771,7 +1778,7 @@ class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', (
|
|||||||
def _oauth2_web_server_flow_params(kwargs):
|
def _oauth2_web_server_flow_params(kwargs):
|
||||||
"""Configures redirect URI parameters for OAuth2WebServerFlow."""
|
"""Configures redirect URI parameters for OAuth2WebServerFlow."""
|
||||||
params = {
|
params = {
|
||||||
# 'access_type': 'offline',
|
'access_type': 'offline',
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2059,15 +2066,17 @@ class OAuth2WebServerFlow(Flow):
|
|||||||
token_expiry = delta + _UTCNOW()
|
token_expiry = delta + _UTCNOW()
|
||||||
|
|
||||||
extracted_id_token = None
|
extracted_id_token = None
|
||||||
|
id_token_jwt = None
|
||||||
if 'id_token' in d:
|
if 'id_token' in d:
|
||||||
extracted_id_token = _extract_id_token(d['id_token'])
|
extracted_id_token = _extract_id_token(d['id_token'])
|
||||||
|
id_token_jwt = d['id_token']
|
||||||
|
|
||||||
logger.info('Successfully retrieved access token')
|
logger.info('Successfully retrieved access token')
|
||||||
return OAuth2Credentials(
|
return OAuth2Credentials(
|
||||||
access_token, self.client_id, self.client_secret,
|
access_token, self.client_id, self.client_secret,
|
||||||
refresh_token, token_expiry, self.token_uri, self.user_agent,
|
refresh_token, token_expiry, self.token_uri, self.user_agent,
|
||||||
revoke_uri=self.revoke_uri, id_token=extracted_id_token,
|
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)
|
token_info_uri=self.token_info_uri)
|
||||||
else:
|
else:
|
||||||
logger.info('Failed to retrieve access token: %s', content)
|
logger.info('Failed to retrieve access token: %s', content)
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -19,6 +19,7 @@ See https://cloud.google.com/compute/docs/metadata
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
from six.moves import http_client
|
from six.moves import http_client
|
||||||
from six.moves.urllib import parse as urlparse
|
from six.moves.urllib import parse as urlparse
|
||||||
@@ -28,7 +29,8 @@ from oauth2client import client
|
|||||||
from oauth2client import transport
|
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'}
|
METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +40,7 @@ def get(http, path, root=METADATA_ROOT, recursive=None):
|
|||||||
Args:
|
Args:
|
||||||
http: an object to be used to make HTTP requests.
|
http: an object to be used to make HTTP requests.
|
||||||
path: A string indicating the resource to retrieve. For example,
|
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.
|
root: A string indicating the full path to the metadata server root.
|
||||||
recursive: A boolean indicating whether to do a recursive query of
|
recursive: A boolean indicating whether to do a recursive query of
|
||||||
metadata. See
|
metadata. See
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -37,6 +37,7 @@ class CommunicationError(Error):
|
|||||||
class NoDevshellServer(Error):
|
class NoDevshellServer(Error):
|
||||||
"""Error when no Developer Shell server can be contacted."""
|
"""Error when no Developer Shell server can be contacted."""
|
||||||
|
|
||||||
|
|
||||||
# The request for credential information to the Developer Shell client socket
|
# 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
|
# is always an empty PBLite-formatted JSON object, so just define it as a
|
||||||
# constant.
|
# constant.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import pickle
|
|||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import encoding
|
from django.utils import encoding
|
||||||
|
import jsonpickle
|
||||||
|
|
||||||
import oauth2client
|
import oauth2client
|
||||||
|
|
||||||
@@ -48,7 +49,12 @@ class CredentialsField(models.Field):
|
|||||||
elif isinstance(value, oauth2client.client.Credentials):
|
elif isinstance(value, oauth2client.client.Credentials):
|
||||||
return value
|
return value
|
||||||
else:
|
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):
|
def get_prep_value(self, value):
|
||||||
"""Overrides ``models.Field`` method. This is used to convert
|
"""Overrides ``models.Field`` method. This is used to convert
|
||||||
@@ -58,7 +64,8 @@ class CredentialsField(models.Field):
|
|||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
else:
|
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):
|
def value_to_string(self, obj):
|
||||||
"""Convert the field value from the provided model to a string.
|
"""Convert the field value from the provided model to a string.
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ try:
|
|||||||
from flask import request
|
from flask import request
|
||||||
from flask import session
|
from flask import session
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
import markupsafe
|
||||||
except ImportError: # pragma: NO COVER
|
except ImportError: # pragma: NO COVER
|
||||||
raise ImportError('The flask utilities require flask 0.9 or newer.')
|
raise ImportError('The flask utilities require flask 0.9 or newer.')
|
||||||
|
|
||||||
@@ -388,6 +389,7 @@ class UserOAuth2(object):
|
|||||||
if 'error' in request.args:
|
if 'error' in request.args:
|
||||||
reason = request.args.get(
|
reason = request.args.get(
|
||||||
'error_description', request.args.get('error', ''))
|
'error_description', request.args.get('error', ''))
|
||||||
|
reason = markupsafe.escape(reason)
|
||||||
return ('Authorization failed: {0}'.format(reason),
|
return ('Authorization failed: {0}'.format(reason),
|
||||||
httplib.BAD_REQUEST)
|
httplib.BAD_REQUEST)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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': '<client id>',
|
|
||||||
'userAgent': '<user agent>',
|
|
||||||
'scope': '<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)
|
|
||||||
@@ -92,6 +92,7 @@ def _CreateArgumentParser():
|
|||||||
help='Set the logging level of detail.')
|
help='Set the logging level of detail.')
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
# argparser is an ArgumentParser that contains command-line options expected
|
# 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
|
# by tools.run(). Pass it in as part of the 'parents' argument to your own
|
||||||
# ArgumentParser.
|
# ArgumentParser.
|
||||||
@@ -217,16 +218,6 @@ def run_flow(flow, storage, flags=None, http=None):
|
|||||||
flow.redirect_uri = oauth_callback
|
flow.redirect_uri = oauth_callback
|
||||||
authorize_url = flow.step1_get_authorize_url()
|
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:
|
if not flags.noauth_local_webserver:
|
||||||
import webbrowser
|
import webbrowser
|
||||||
webbrowser.open(authorize_url, new=1, autoraise=True)
|
webbrowser.open(authorize_url, new=1, autoraise=True)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
Reference in New Issue
Block a user