Updated DB_Helper by adding firebase methods.
This commit is contained in:
parent
485cc3bbba
commit
c82121d036
1810 changed files with 537281 additions and 1 deletions
23
venv/Lib/site-packages/oauth2client/__init__.py
Normal file
23
venv/Lib/site-packages/oauth2client/__init__.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Copyright 2015 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.
|
||||
|
||||
"""Client library for using OAuth2, especially with Google APIs."""
|
||||
|
||||
__version__ = '3.0.0'
|
||||
|
||||
GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth'
|
||||
GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code'
|
||||
GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke'
|
||||
GOOGLE_TOKEN_URI = 'https://www.googleapis.com/oauth2/v4/token'
|
||||
GOOGLE_TOKEN_INFO_URI = 'https://www.googleapis.com/oauth2/v3/tokeninfo'
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
105
venv/Lib/site-packages/oauth2client/_helpers.py
Normal file
105
venv/Lib/site-packages/oauth2client/_helpers.py
Normal file
|
@ -0,0 +1,105 @@
|
|||
# Copyright 2015 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.
|
||||
"""Helper functions for commonly used utilities."""
|
||||
|
||||
import base64
|
||||
import json
|
||||
|
||||
import six
|
||||
|
||||
|
||||
def _parse_pem_key(raw_key_input):
|
||||
"""Identify and extract PEM keys.
|
||||
|
||||
Determines whether the given key is in the format of PEM key, and extracts
|
||||
the relevant part of the key if it is.
|
||||
|
||||
Args:
|
||||
raw_key_input: The contents of a private key file (either PEM or
|
||||
PKCS12).
|
||||
|
||||
Returns:
|
||||
string, The actual key if the contents are from a PEM file, or
|
||||
else None.
|
||||
"""
|
||||
offset = raw_key_input.find(b'-----BEGIN ')
|
||||
if offset != -1:
|
||||
return raw_key_input[offset:]
|
||||
|
||||
|
||||
def _json_encode(data):
|
||||
return json.dumps(data, separators=(',', ':'))
|
||||
|
||||
|
||||
def _to_bytes(value, encoding='ascii'):
|
||||
"""Converts a string value to bytes, if necessary.
|
||||
|
||||
Unfortunately, ``six.b`` is insufficient for this task since in
|
||||
Python2 it does not modify ``unicode`` objects.
|
||||
|
||||
Args:
|
||||
value: The string/bytes value to be converted.
|
||||
encoding: The encoding to use to convert unicode to bytes. Defaults
|
||||
to "ascii", which will not allow any characters from ordinals
|
||||
larger than 127. Other useful values are "latin-1", which
|
||||
which will only allows byte ordinals (up to 255) and "utf-8",
|
||||
which will encode any unicode that needs to be.
|
||||
|
||||
Returns:
|
||||
The original value converted to bytes (if unicode) or as passed in
|
||||
if it started out as bytes.
|
||||
|
||||
Raises:
|
||||
ValueError if the value could not be converted to bytes.
|
||||
"""
|
||||
result = (value.encode(encoding)
|
||||
if isinstance(value, six.text_type) else value)
|
||||
if isinstance(result, six.binary_type):
|
||||
return result
|
||||
else:
|
||||
raise ValueError('{0!r} could not be converted to bytes'.format(value))
|
||||
|
||||
|
||||
def _from_bytes(value):
|
||||
"""Converts bytes to a string value, if necessary.
|
||||
|
||||
Args:
|
||||
value: The string/bytes value to be converted.
|
||||
|
||||
Returns:
|
||||
The original value converted to unicode (if bytes) or as passed in
|
||||
if it started out as unicode.
|
||||
|
||||
Raises:
|
||||
ValueError if the value could not be converted to unicode.
|
||||
"""
|
||||
result = (value.decode('utf-8')
|
||||
if isinstance(value, six.binary_type) else value)
|
||||
if isinstance(result, six.text_type):
|
||||
return result
|
||||
else:
|
||||
raise ValueError(
|
||||
'{0!r} could not be converted to unicode'.format(value))
|
||||
|
||||
|
||||
def _urlsafe_b64encode(raw_bytes):
|
||||
raw_bytes = _to_bytes(raw_bytes, encoding='utf-8')
|
||||
return base64.urlsafe_b64encode(raw_bytes).rstrip(b'=')
|
||||
|
||||
|
||||
def _urlsafe_b64decode(b64string):
|
||||
# Guard against unicode strings, which base64 can't handle.
|
||||
b64string = _to_bytes(b64string)
|
||||
padded = b64string + b'=' * (4 - len(b64string) % 4)
|
||||
return base64.urlsafe_b64decode(padded)
|
136
venv/Lib/site-packages/oauth2client/_openssl_crypt.py
Normal file
136
venv/Lib/site-packages/oauth2client/_openssl_crypt.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
# Copyright 2015 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.
|
||||
"""OpenSSL Crypto-related routines for oauth2client."""
|
||||
|
||||
from OpenSSL import crypto
|
||||
|
||||
from oauth2client import _helpers
|
||||
|
||||
|
||||
class OpenSSLVerifier(object):
|
||||
"""Verifies the signature on a message."""
|
||||
|
||||
def __init__(self, pubkey):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
pubkey: OpenSSL.crypto.PKey, The public key to verify with.
|
||||
"""
|
||||
self._pubkey = pubkey
|
||||
|
||||
def verify(self, message, signature):
|
||||
"""Verifies a message against a signature.
|
||||
|
||||
Args:
|
||||
message: string or bytes, The message to verify. If string, will be
|
||||
encoded to bytes as utf-8.
|
||||
signature: string or bytes, The signature on the message. If string,
|
||||
will be encoded to bytes as utf-8.
|
||||
|
||||
Returns:
|
||||
True if message was signed by the private key associated with the
|
||||
public key that this object was constructed with.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
signature = _helpers._to_bytes(signature, encoding='utf-8')
|
||||
try:
|
||||
crypto.verify(self._pubkey, signature, message, 'sha256')
|
||||
return True
|
||||
except crypto.Error:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def from_string(key_pem, is_x509_cert):
|
||||
"""Construct a Verified instance from a string.
|
||||
|
||||
Args:
|
||||
key_pem: string, public key in PEM format.
|
||||
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
|
||||
is expected to be an RSA key in PEM format.
|
||||
|
||||
Returns:
|
||||
Verifier instance.
|
||||
|
||||
Raises:
|
||||
OpenSSL.crypto.Error: if the key_pem can't be parsed.
|
||||
"""
|
||||
key_pem = _helpers._to_bytes(key_pem)
|
||||
if is_x509_cert:
|
||||
pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
|
||||
else:
|
||||
pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
|
||||
return OpenSSLVerifier(pubkey)
|
||||
|
||||
|
||||
class OpenSSLSigner(object):
|
||||
"""Signs messages with a private key."""
|
||||
|
||||
def __init__(self, pkey):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
pkey: OpenSSL.crypto.PKey (or equiv), The private key to sign with.
|
||||
"""
|
||||
self._key = pkey
|
||||
|
||||
def sign(self, message):
|
||||
"""Signs a message.
|
||||
|
||||
Args:
|
||||
message: bytes, Message to be signed.
|
||||
|
||||
Returns:
|
||||
string, The signature of the message for the given key.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
return crypto.sign(self._key, message, 'sha256')
|
||||
|
||||
@staticmethod
|
||||
def from_string(key, password=b'notasecret'):
|
||||
"""Construct a Signer instance from a string.
|
||||
|
||||
Args:
|
||||
key: string, private key in PKCS12 or PEM format.
|
||||
password: string, password for the private key file.
|
||||
|
||||
Returns:
|
||||
Signer instance.
|
||||
|
||||
Raises:
|
||||
OpenSSL.crypto.Error if the key can't be parsed.
|
||||
"""
|
||||
key = _helpers._to_bytes(key)
|
||||
parsed_pem_key = _helpers._parse_pem_key(key)
|
||||
if parsed_pem_key:
|
||||
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key)
|
||||
else:
|
||||
password = _helpers._to_bytes(password, encoding='utf-8')
|
||||
pkey = crypto.load_pkcs12(key, password).get_privatekey()
|
||||
return OpenSSLSigner(pkey)
|
||||
|
||||
|
||||
def pkcs12_key_as_pem(private_key_bytes, private_key_password):
|
||||
"""Convert the contents of a PKCS#12 key to PEM using pyOpenSSL.
|
||||
|
||||
Args:
|
||||
private_key_bytes: Bytes. PKCS#12 key in DER format.
|
||||
private_key_password: String. Password for PKCS#12 key.
|
||||
|
||||
Returns:
|
||||
String. PEM contents of ``private_key_bytes``.
|
||||
"""
|
||||
private_key_password = _helpers._to_bytes(private_key_password)
|
||||
pkcs12 = crypto.load_pkcs12(private_key_bytes, private_key_password)
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM,
|
||||
pkcs12.get_privatekey())
|
184
venv/Lib/site-packages/oauth2client/_pure_python_crypt.py
Normal file
184
venv/Lib/site-packages/oauth2client/_pure_python_crypt.py
Normal file
|
@ -0,0 +1,184 @@
|
|||
# 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.
|
||||
|
||||
"""Pure Python crypto-related routines for oauth2client.
|
||||
|
||||
Uses the ``rsa``, ``pyasn1`` and ``pyasn1_modules`` packages
|
||||
to parse PEM files storing PKCS#1 or PKCS#8 keys as well as
|
||||
certificates.
|
||||
"""
|
||||
|
||||
from pyasn1.codec.der import decoder
|
||||
from pyasn1_modules import pem
|
||||
from pyasn1_modules.rfc2459 import Certificate
|
||||
from pyasn1_modules.rfc5208 import PrivateKeyInfo
|
||||
import rsa
|
||||
import six
|
||||
|
||||
from oauth2client import _helpers
|
||||
|
||||
|
||||
_PKCS12_ERROR = r"""\
|
||||
PKCS12 format is not supported by the RSA library.
|
||||
Either install PyOpenSSL, or please convert .p12 format
|
||||
to .pem format:
|
||||
$ cat key.p12 | \
|
||||
> openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
|
||||
> openssl rsa > key.pem
|
||||
"""
|
||||
|
||||
_POW2 = (128, 64, 32, 16, 8, 4, 2, 1)
|
||||
_PKCS1_MARKER = ('-----BEGIN RSA PRIVATE KEY-----',
|
||||
'-----END RSA PRIVATE KEY-----')
|
||||
_PKCS8_MARKER = ('-----BEGIN PRIVATE KEY-----',
|
||||
'-----END PRIVATE KEY-----')
|
||||
_PKCS8_SPEC = PrivateKeyInfo()
|
||||
|
||||
|
||||
def _bit_list_to_bytes(bit_list):
|
||||
"""Converts an iterable of 1's and 0's to bytes.
|
||||
|
||||
Combines the list 8 at a time, treating each group of 8 bits
|
||||
as a single byte.
|
||||
"""
|
||||
num_bits = len(bit_list)
|
||||
byte_vals = bytearray()
|
||||
for start in six.moves.xrange(0, num_bits, 8):
|
||||
curr_bits = bit_list[start:start + 8]
|
||||
char_val = sum(val * digit
|
||||
for val, digit in zip(_POW2, curr_bits))
|
||||
byte_vals.append(char_val)
|
||||
return bytes(byte_vals)
|
||||
|
||||
|
||||
class RsaVerifier(object):
|
||||
"""Verifies the signature on a message.
|
||||
|
||||
Args:
|
||||
pubkey: rsa.key.PublicKey (or equiv), The public key to verify with.
|
||||
"""
|
||||
|
||||
def __init__(self, pubkey):
|
||||
self._pubkey = pubkey
|
||||
|
||||
def verify(self, message, signature):
|
||||
"""Verifies a message against a signature.
|
||||
|
||||
Args:
|
||||
message: string or bytes, The message to verify. If string, will be
|
||||
encoded to bytes as utf-8.
|
||||
signature: string or bytes, The signature on the message. If
|
||||
string, will be encoded to bytes as utf-8.
|
||||
|
||||
Returns:
|
||||
True if message was signed by the private key associated with the
|
||||
public key that this object was constructed with.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
try:
|
||||
return rsa.pkcs1.verify(message, signature, self._pubkey)
|
||||
except (ValueError, rsa.pkcs1.VerificationError):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, key_pem, is_x509_cert):
|
||||
"""Construct an RsaVerifier instance from a string.
|
||||
|
||||
Args:
|
||||
key_pem: string, public key in PEM format.
|
||||
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
|
||||
is expected to be an RSA key in PEM format.
|
||||
|
||||
Returns:
|
||||
RsaVerifier instance.
|
||||
|
||||
Raises:
|
||||
ValueError: if the key_pem can't be parsed. In either case, error
|
||||
will begin with 'No PEM start marker'. If
|
||||
``is_x509_cert`` is True, will fail to find the
|
||||
"-----BEGIN CERTIFICATE-----" error, otherwise fails
|
||||
to find "-----BEGIN RSA PUBLIC KEY-----".
|
||||
"""
|
||||
key_pem = _helpers._to_bytes(key_pem)
|
||||
if is_x509_cert:
|
||||
der = rsa.pem.load_pem(key_pem, 'CERTIFICATE')
|
||||
asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate())
|
||||
if remaining != b'':
|
||||
raise ValueError('Unused bytes', remaining)
|
||||
|
||||
cert_info = asn1_cert['tbsCertificate']['subjectPublicKeyInfo']
|
||||
key_bytes = _bit_list_to_bytes(cert_info['subjectPublicKey'])
|
||||
pubkey = rsa.PublicKey.load_pkcs1(key_bytes, 'DER')
|
||||
else:
|
||||
pubkey = rsa.PublicKey.load_pkcs1(key_pem, 'PEM')
|
||||
return cls(pubkey)
|
||||
|
||||
|
||||
class RsaSigner(object):
|
||||
"""Signs messages with a private key.
|
||||
|
||||
Args:
|
||||
pkey: rsa.key.PrivateKey (or equiv), The private key to sign with.
|
||||
"""
|
||||
|
||||
def __init__(self, pkey):
|
||||
self._key = pkey
|
||||
|
||||
def sign(self, message):
|
||||
"""Signs a message.
|
||||
|
||||
Args:
|
||||
message: bytes, Message to be signed.
|
||||
|
||||
Returns:
|
||||
string, The signature of the message for the given key.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
return rsa.pkcs1.sign(message, self._key, 'SHA-256')
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, key, password='notasecret'):
|
||||
"""Construct an RsaSigner instance from a string.
|
||||
|
||||
Args:
|
||||
key: string, private key in PEM format.
|
||||
password: string, password for private key file. Unused for PEM
|
||||
files.
|
||||
|
||||
Returns:
|
||||
RsaSigner instance.
|
||||
|
||||
Raises:
|
||||
ValueError if the key cannot be parsed as PKCS#1 or PKCS#8 in
|
||||
PEM format.
|
||||
"""
|
||||
key = _helpers._from_bytes(key) # pem expects str in Py3
|
||||
marker_id, key_bytes = pem.readPemBlocksFromFile(
|
||||
six.StringIO(key), _PKCS1_MARKER, _PKCS8_MARKER)
|
||||
|
||||
if marker_id == 0:
|
||||
pkey = rsa.key.PrivateKey.load_pkcs1(key_bytes,
|
||||
format='DER')
|
||||
elif marker_id == 1:
|
||||
key_info, remaining = decoder.decode(
|
||||
key_bytes, asn1Spec=_PKCS8_SPEC)
|
||||
if remaining != b'':
|
||||
raise ValueError('Unused bytes', remaining)
|
||||
pkey_info = key_info.getComponentByName('privateKey')
|
||||
pkey = rsa.key.PrivateKey.load_pkcs1(pkey_info.asOctets(),
|
||||
format='DER')
|
||||
else:
|
||||
raise ValueError('No key could be detected.')
|
||||
|
||||
return cls(pkey)
|
124
venv/Lib/site-packages/oauth2client/_pycrypto_crypt.py
Normal file
124
venv/Lib/site-packages/oauth2client/_pycrypto_crypt.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
# Copyright 2015 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.
|
||||
"""pyCrypto Crypto-related routines for oauth2client."""
|
||||
|
||||
from Crypto.Hash import SHA256
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
from Crypto.Util.asn1 import DerSequence
|
||||
|
||||
from oauth2client import _helpers
|
||||
|
||||
|
||||
class PyCryptoVerifier(object):
|
||||
"""Verifies the signature on a message."""
|
||||
|
||||
def __init__(self, pubkey):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
pubkey: OpenSSL.crypto.PKey (or equiv), The public key to verify
|
||||
with.
|
||||
"""
|
||||
self._pubkey = pubkey
|
||||
|
||||
def verify(self, message, signature):
|
||||
"""Verifies a message against a signature.
|
||||
|
||||
Args:
|
||||
message: string or bytes, The message to verify. If string, will be
|
||||
encoded to bytes as utf-8.
|
||||
signature: string or bytes, The signature on the message.
|
||||
|
||||
Returns:
|
||||
True if message was signed by the private key associated with the
|
||||
public key that this object was constructed with.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
return PKCS1_v1_5.new(self._pubkey).verify(
|
||||
SHA256.new(message), signature)
|
||||
|
||||
@staticmethod
|
||||
def from_string(key_pem, is_x509_cert):
|
||||
"""Construct a Verified instance from a string.
|
||||
|
||||
Args:
|
||||
key_pem: string, public key in PEM format.
|
||||
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
|
||||
is expected to be an RSA key in PEM format.
|
||||
|
||||
Returns:
|
||||
Verifier instance.
|
||||
"""
|
||||
if is_x509_cert:
|
||||
key_pem = _helpers._to_bytes(key_pem)
|
||||
pemLines = key_pem.replace(b' ', b'').split()
|
||||
certDer = _helpers._urlsafe_b64decode(b''.join(pemLines[1:-1]))
|
||||
certSeq = DerSequence()
|
||||
certSeq.decode(certDer)
|
||||
tbsSeq = DerSequence()
|
||||
tbsSeq.decode(certSeq[0])
|
||||
pubkey = RSA.importKey(tbsSeq[6])
|
||||
else:
|
||||
pubkey = RSA.importKey(key_pem)
|
||||
return PyCryptoVerifier(pubkey)
|
||||
|
||||
|
||||
class PyCryptoSigner(object):
|
||||
"""Signs messages with a private key."""
|
||||
|
||||
def __init__(self, pkey):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
|
||||
"""
|
||||
self._key = pkey
|
||||
|
||||
def sign(self, message):
|
||||
"""Signs a message.
|
||||
|
||||
Args:
|
||||
message: string, Message to be signed.
|
||||
|
||||
Returns:
|
||||
string, The signature of the message for the given key.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
return PKCS1_v1_5.new(self._key).sign(SHA256.new(message))
|
||||
|
||||
@staticmethod
|
||||
def from_string(key, password='notasecret'):
|
||||
"""Construct a Signer instance from a string.
|
||||
|
||||
Args:
|
||||
key: string, private key in PEM format.
|
||||
password: string, password for private key file. Unused for PEM
|
||||
files.
|
||||
|
||||
Returns:
|
||||
Signer instance.
|
||||
|
||||
Raises:
|
||||
NotImplementedError if the key isn't in PEM format.
|
||||
"""
|
||||
parsed_pem_key = _helpers._parse_pem_key(_helpers._to_bytes(key))
|
||||
if parsed_pem_key:
|
||||
pkey = RSA.importKey(parsed_pem_key)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
'No key in PEM format was detected. This implementation '
|
||||
'can only use the PyCrypto library for keys in PEM '
|
||||
'format.')
|
||||
return PyCryptoSigner(pkey)
|
2133
venv/Lib/site-packages/oauth2client/client.py
Normal file
2133
venv/Lib/site-packages/oauth2client/client.py
Normal file
File diff suppressed because it is too large
Load diff
174
venv/Lib/site-packages/oauth2client/clientsecrets.py
Normal file
174
venv/Lib/site-packages/oauth2client/clientsecrets.py
Normal file
|
@ -0,0 +1,174 @@
|
|||
# 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.
|
||||
|
||||
"""Utilities for reading OAuth 2.0 client secret files.
|
||||
|
||||
A client_secrets.json file contains all the information needed to interact with
|
||||
an OAuth 2.0 protected service.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import six
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
# Properties that make a client_secrets.json file valid.
|
||||
TYPE_WEB = 'web'
|
||||
TYPE_INSTALLED = 'installed'
|
||||
|
||||
VALID_CLIENT = {
|
||||
TYPE_WEB: {
|
||||
'required': [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'redirect_uris',
|
||||
'auth_uri',
|
||||
'token_uri',
|
||||
],
|
||||
'string': [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
],
|
||||
},
|
||||
TYPE_INSTALLED: {
|
||||
'required': [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'redirect_uris',
|
||||
'auth_uri',
|
||||
'token_uri',
|
||||
],
|
||||
'string': [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Base error for this module."""
|
||||
|
||||
|
||||
class InvalidClientSecretsError(Error):
|
||||
"""Format of ClientSecrets file is invalid."""
|
||||
|
||||
|
||||
def _validate_clientsecrets(clientsecrets_dict):
|
||||
"""Validate parsed client secrets from a file.
|
||||
|
||||
Args:
|
||||
clientsecrets_dict: dict, a dictionary holding the client secrets.
|
||||
|
||||
Returns:
|
||||
tuple, a string of the client type and the information parsed
|
||||
from the file.
|
||||
"""
|
||||
_INVALID_FILE_FORMAT_MSG = (
|
||||
'Invalid file format. See '
|
||||
'https://developers.google.com/api-client-library/'
|
||||
'python/guide/aaa_client_secrets')
|
||||
|
||||
if clientsecrets_dict is None:
|
||||
raise InvalidClientSecretsError(_INVALID_FILE_FORMAT_MSG)
|
||||
try:
|
||||
(client_type, client_info), = clientsecrets_dict.items()
|
||||
except (ValueError, AttributeError):
|
||||
raise InvalidClientSecretsError(
|
||||
_INVALID_FILE_FORMAT_MSG + ' '
|
||||
'Expected a JSON object with a single property for a "web" or '
|
||||
'"installed" application')
|
||||
|
||||
if client_type not in VALID_CLIENT:
|
||||
raise InvalidClientSecretsError(
|
||||
'Unknown client type: {0}.'.format(client_type))
|
||||
|
||||
for prop_name in VALID_CLIENT[client_type]['required']:
|
||||
if prop_name not in client_info:
|
||||
raise InvalidClientSecretsError(
|
||||
'Missing property "{0}" in a client type of "{1}".'.format(
|
||||
prop_name, client_type))
|
||||
for prop_name in VALID_CLIENT[client_type]['string']:
|
||||
if client_info[prop_name].startswith('[['):
|
||||
raise InvalidClientSecretsError(
|
||||
'Property "{0}" is not configured.'.format(prop_name))
|
||||
return client_type, client_info
|
||||
|
||||
|
||||
def load(fp):
|
||||
obj = json.load(fp)
|
||||
return _validate_clientsecrets(obj)
|
||||
|
||||
|
||||
def loads(s):
|
||||
obj = json.loads(s)
|
||||
return _validate_clientsecrets(obj)
|
||||
|
||||
|
||||
def _loadfile(filename):
|
||||
try:
|
||||
with open(filename, 'r') as fp:
|
||||
obj = json.load(fp)
|
||||
except IOError as exc:
|
||||
raise InvalidClientSecretsError('Error opening file', exc.filename,
|
||||
exc.strerror, exc.errno)
|
||||
return _validate_clientsecrets(obj)
|
||||
|
||||
|
||||
def loadfile(filename, cache=None):
|
||||
"""Loading of client_secrets JSON file, optionally backed by a cache.
|
||||
|
||||
Typical cache storage would be App Engine memcache service,
|
||||
but you can pass in any other cache client that implements
|
||||
these methods:
|
||||
|
||||
* ``get(key, namespace=ns)``
|
||||
* ``set(key, value, namespace=ns)``
|
||||
|
||||
Usage::
|
||||
|
||||
# without caching
|
||||
client_type, client_info = loadfile('secrets.json')
|
||||
# using App Engine memcache service
|
||||
from google.appengine.api import memcache
|
||||
client_type, client_info = loadfile('secrets.json', cache=memcache)
|
||||
|
||||
Args:
|
||||
filename: string, Path to a client_secrets.json file on a filesystem.
|
||||
cache: An optional cache service client that implements get() and set()
|
||||
methods. If not specified, the file is always being loaded from
|
||||
a filesystem.
|
||||
|
||||
Raises:
|
||||
InvalidClientSecretsError: In case of a validation error or some
|
||||
I/O failure. Can happen only on cache miss.
|
||||
|
||||
Returns:
|
||||
(client_type, client_info) tuple, as _loadfile() normally would.
|
||||
JSON contents is validated only during first load. Cache hits are not
|
||||
validated.
|
||||
"""
|
||||
_SECRET_NAMESPACE = 'oauth2client:secrets#ns'
|
||||
|
||||
if not cache:
|
||||
return _loadfile(filename)
|
||||
|
||||
obj = cache.get(filename, namespace=_SECRET_NAMESPACE)
|
||||
if obj is None:
|
||||
client_type, client_info = _loadfile(filename)
|
||||
obj = {client_type: client_info}
|
||||
cache.set(filename, obj, namespace=_SECRET_NAMESPACE)
|
||||
|
||||
return next(six.iteritems(obj))
|
6
venv/Lib/site-packages/oauth2client/contrib/__init__.py
Normal file
6
venv/Lib/site-packages/oauth2client/contrib/__init__.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
"""Contributed modules.
|
||||
|
||||
Contrib contains modules that are not considered part of the core oauth2client
|
||||
library but provide additional functionality. These modules are intended to
|
||||
make it easier to use oauth2client.
|
||||
"""
|
163
venv/Lib/site-packages/oauth2client/contrib/_appengine_ndb.py
Normal file
163
venv/Lib/site-packages/oauth2client/contrib/_appengine_ndb.py
Normal file
|
@ -0,0 +1,163 @@
|
|||
# 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.
|
||||
|
||||
"""Google App Engine utilities helper.
|
||||
|
||||
Classes that directly require App Engine's ndb library. Provided
|
||||
as a separate module in case of failure to import ndb while
|
||||
other App Engine libraries are present.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from google.appengine.ext import ndb
|
||||
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
NDB_KEY = ndb.Key
|
||||
"""Key constant used by :mod:`oauth2client.contrib.appengine`."""
|
||||
|
||||
NDB_MODEL = ndb.Model
|
||||
"""Model constant used by :mod:`oauth2client.contrib.appengine`."""
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SiteXsrfSecretKeyNDB(ndb.Model):
|
||||
"""NDB Model for storage for the sites XSRF secret key.
|
||||
|
||||
Since this model uses the same kind as SiteXsrfSecretKey, it can be
|
||||
used interchangeably. This simply provides an NDB model for interacting
|
||||
with the same data the DB model interacts with.
|
||||
|
||||
There should only be one instance stored of this model, the one used
|
||||
for the site.
|
||||
"""
|
||||
secret = ndb.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def _get_kind(cls):
|
||||
"""Return the kind name for this class."""
|
||||
return 'SiteXsrfSecretKey'
|
||||
|
||||
|
||||
class FlowNDBProperty(ndb.PickleProperty):
|
||||
"""App Engine NDB datastore Property for Flow.
|
||||
|
||||
Serves the same purpose as the DB FlowProperty, but for NDB models.
|
||||
Since PickleProperty inherits from BlobProperty, the underlying
|
||||
representation of the data in the datastore will be the same as in the
|
||||
DB case.
|
||||
|
||||
Utility property that allows easy storage and retrieval of an
|
||||
oauth2client.Flow
|
||||
"""
|
||||
|
||||
def _validate(self, value):
|
||||
"""Validates a value as a proper Flow object.
|
||||
|
||||
Args:
|
||||
value: A value to be set on the property.
|
||||
|
||||
Raises:
|
||||
TypeError if the value is not an instance of Flow.
|
||||
"""
|
||||
_LOGGER.info('validate: Got type %s', type(value))
|
||||
if value is not None and not isinstance(value, client.Flow):
|
||||
raise TypeError(
|
||||
'Property {0} must be convertible to a flow '
|
||||
'instance; received: {1}.'.format(self._name, value))
|
||||
|
||||
|
||||
class CredentialsNDBProperty(ndb.BlobProperty):
|
||||
"""App Engine NDB datastore Property for Credentials.
|
||||
|
||||
Serves the same purpose as the DB CredentialsProperty, but for NDB
|
||||
models. Since CredentialsProperty stores data as a blob and this
|
||||
inherits from BlobProperty, the data in the datastore will be the same
|
||||
as in the DB case.
|
||||
|
||||
Utility property that allows easy storage and retrieval of Credentials
|
||||
and subclasses.
|
||||
"""
|
||||
|
||||
def _validate(self, value):
|
||||
"""Validates a value as a proper credentials object.
|
||||
|
||||
Args:
|
||||
value: A value to be set on the property.
|
||||
|
||||
Raises:
|
||||
TypeError if the value is not an instance of Credentials.
|
||||
"""
|
||||
_LOGGER.info('validate: Got type %s', type(value))
|
||||
if value is not None and not isinstance(value, client.Credentials):
|
||||
raise TypeError(
|
||||
'Property {0} must be convertible to a credentials '
|
||||
'instance; received: {1}.'.format(self._name, value))
|
||||
|
||||
def _to_base_type(self, value):
|
||||
"""Converts our validated value to a JSON serialized string.
|
||||
|
||||
Args:
|
||||
value: A value to be set in the datastore.
|
||||
|
||||
Returns:
|
||||
A JSON serialized version of the credential, else '' if value
|
||||
is None.
|
||||
"""
|
||||
if value is None:
|
||||
return ''
|
||||
else:
|
||||
return value.to_json()
|
||||
|
||||
def _from_base_type(self, value):
|
||||
"""Converts our stored JSON string back to the desired type.
|
||||
|
||||
Args:
|
||||
value: A value from the datastore to be converted to the
|
||||
desired type.
|
||||
|
||||
Returns:
|
||||
A deserialized Credentials (or subclass) object, else None if
|
||||
the value can't be parsed.
|
||||
"""
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
# Uses the from_json method of the implied class of value
|
||||
credentials = client.Credentials.new_from_json(value)
|
||||
except ValueError:
|
||||
credentials = None
|
||||
return credentials
|
||||
|
||||
|
||||
class CredentialsNDBModel(ndb.Model):
|
||||
"""NDB Model for storage of OAuth 2.0 Credentials
|
||||
|
||||
Since this model uses the same kind as CredentialsModel and has a
|
||||
property which can serialize and deserialize Credentials correctly, it
|
||||
can be used interchangeably with a CredentialsModel to access, insert
|
||||
and delete the same entities. This simply provides an NDB model for
|
||||
interacting with the same data the DB model interacts with.
|
||||
|
||||
Storage of the model is keyed by the user.user_id().
|
||||
"""
|
||||
credentials = CredentialsNDBProperty()
|
||||
|
||||
@classmethod
|
||||
def _get_kind(cls):
|
||||
"""Return the kind name for this class."""
|
||||
return 'CredentialsModel'
|
81
venv/Lib/site-packages/oauth2client/contrib/_fcntl_opener.py
Normal file
81
venv/Lib/site-packages/oauth2client/contrib/_fcntl_opener.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
# 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()
|
123
venv/Lib/site-packages/oauth2client/contrib/_metadata.py
Normal file
123
venv/Lib/site-packages/oauth2client/contrib/_metadata.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
# 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.
|
||||
|
||||
"""Provides helper methods for talking to the Compute Engine metadata server.
|
||||
|
||||
See https://cloud.google.com/compute/docs/metadata
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
|
||||
import httplib2
|
||||
from six.moves import http_client
|
||||
from six.moves.urllib import parse as urlparse
|
||||
|
||||
from oauth2client import _helpers
|
||||
from oauth2client import client
|
||||
from oauth2client import util
|
||||
|
||||
|
||||
METADATA_ROOT = 'http://metadata.google.internal/computeMetadata/v1/'
|
||||
METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
|
||||
|
||||
|
||||
def get(http_request, path, root=METADATA_ROOT, recursive=None):
|
||||
"""Fetch a resource from the metadata server.
|
||||
|
||||
Args:
|
||||
path: A string indicating the resource to retrieve. For example,
|
||||
'instance/service-accounts/defualt'
|
||||
http_request: A callable that matches the method
|
||||
signature of httplib2.Http.request. Used to make the request to the
|
||||
metadataserver.
|
||||
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
|
||||
https://cloud.google.com/compute/docs/metadata#aggcontents
|
||||
|
||||
Returns:
|
||||
A dictionary if the metadata server returns JSON, otherwise a string.
|
||||
|
||||
Raises:
|
||||
httplib2.Httplib2Error if an error corrured while retrieving metadata.
|
||||
"""
|
||||
url = urlparse.urljoin(root, path)
|
||||
url = util._add_query_parameter(url, 'recursive', recursive)
|
||||
|
||||
response, content = http_request(
|
||||
url,
|
||||
headers=METADATA_HEADERS
|
||||
)
|
||||
|
||||
if response.status == http_client.OK:
|
||||
decoded = _helpers._from_bytes(content)
|
||||
if response['content-type'] == 'application/json':
|
||||
return json.loads(decoded)
|
||||
else:
|
||||
return decoded
|
||||
else:
|
||||
raise httplib2.HttpLib2Error(
|
||||
'Failed to retrieve {0} from the Google Compute Engine'
|
||||
'metadata service. Response:\n{1}'.format(url, response))
|
||||
|
||||
|
||||
def get_service_account_info(http_request, service_account='default'):
|
||||
"""Get information about a service account from the metadata server.
|
||||
|
||||
Args:
|
||||
service_account: An email specifying the service account for which to
|
||||
look up information. Default will be information for the "default"
|
||||
service account of the current compute engine instance.
|
||||
http_request: A callable that matches the method
|
||||
signature of httplib2.Http.request. Used to make the request to the
|
||||
metadata server.
|
||||
Returns:
|
||||
A dictionary with information about the specified service account,
|
||||
for example:
|
||||
|
||||
{
|
||||
'email': '...',
|
||||
'scopes': ['scope', ...],
|
||||
'aliases': ['default', '...']
|
||||
}
|
||||
"""
|
||||
return get(
|
||||
http_request,
|
||||
'instance/service-accounts/{0}/'.format(service_account),
|
||||
recursive=True)
|
||||
|
||||
|
||||
def get_token(http_request, service_account='default'):
|
||||
"""Fetch an oauth token for the
|
||||
|
||||
Args:
|
||||
service_account: An email specifying the service account this token
|
||||
should represent. Default will be a token for the "default" service
|
||||
account of the current compute engine instance.
|
||||
http_request: A callable that matches the method
|
||||
signature of httplib2.Http.request. Used to make the request to the
|
||||
metadataserver.
|
||||
|
||||
Returns:
|
||||
A tuple of (access token, token expiration), where access token is the
|
||||
access token as a string and token expiration is a datetime object
|
||||
that indicates when the access token will expire.
|
||||
"""
|
||||
token_json = get(
|
||||
http_request,
|
||||
'instance/service-accounts/{0}/token'.format(service_account))
|
||||
token_expiry = client._UTCNOW() + datetime.timedelta(
|
||||
seconds=token_json['expires_in'])
|
||||
return token_json['access_token'], token_expiry
|
106
venv/Lib/site-packages/oauth2client/contrib/_win32_opener.py
Normal file
106
venv/Lib/site-packages/oauth2client/contrib/_win32_opener.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
# 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()
|
913
venv/Lib/site-packages/oauth2client/contrib/appengine.py
Normal file
913
venv/Lib/site-packages/oauth2client/contrib/appengine.py
Normal file
|
@ -0,0 +1,913 @@
|
|||
# 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.
|
||||
|
||||
"""Utilities for Google App Engine
|
||||
|
||||
Utilities for making it easier to use OAuth 2.0 on Google App Engine.
|
||||
"""
|
||||
|
||||
import cgi
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import threading
|
||||
|
||||
from google.appengine.api import app_identity
|
||||
from google.appengine.api import memcache
|
||||
from google.appengine.api import users
|
||||
from google.appengine.ext import db
|
||||
from google.appengine.ext.webapp.util import login_required
|
||||
import httplib2
|
||||
import webapp2 as webapp
|
||||
|
||||
import oauth2client
|
||||
from oauth2client import client
|
||||
from oauth2client import clientsecrets
|
||||
from oauth2client import util
|
||||
from oauth2client.contrib import xsrfutil
|
||||
|
||||
# This is a temporary fix for a Google internal issue.
|
||||
try:
|
||||
from oauth2client.contrib import _appengine_ndb
|
||||
except ImportError: # pragma: NO COVER
|
||||
_appengine_ndb = None
|
||||
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
|
||||
|
||||
XSRF_MEMCACHE_ID = 'xsrf_secret_key'
|
||||
|
||||
if _appengine_ndb is None: # pragma: NO COVER
|
||||
CredentialsNDBModel = None
|
||||
CredentialsNDBProperty = None
|
||||
FlowNDBProperty = None
|
||||
_NDB_KEY = None
|
||||
_NDB_MODEL = None
|
||||
SiteXsrfSecretKeyNDB = None
|
||||
else:
|
||||
CredentialsNDBModel = _appengine_ndb.CredentialsNDBModel
|
||||
CredentialsNDBProperty = _appengine_ndb.CredentialsNDBProperty
|
||||
FlowNDBProperty = _appengine_ndb.FlowNDBProperty
|
||||
_NDB_KEY = _appengine_ndb.NDB_KEY
|
||||
_NDB_MODEL = _appengine_ndb.NDB_MODEL
|
||||
SiteXsrfSecretKeyNDB = _appengine_ndb.SiteXsrfSecretKeyNDB
|
||||
|
||||
|
||||
def _safe_html(s):
|
||||
"""Escape text to make it safe to display.
|
||||
|
||||
Args:
|
||||
s: string, The text to escape.
|
||||
|
||||
Returns:
|
||||
The escaped text as a string.
|
||||
"""
|
||||
return cgi.escape(s, quote=1).replace("'", ''')
|
||||
|
||||
|
||||
class SiteXsrfSecretKey(db.Model):
|
||||
"""Storage for the sites XSRF secret key.
|
||||
|
||||
There will only be one instance stored of this model, the one used for the
|
||||
site.
|
||||
"""
|
||||
secret = db.StringProperty()
|
||||
|
||||
|
||||
def _generate_new_xsrf_secret_key():
|
||||
"""Returns a random XSRF secret key."""
|
||||
return os.urandom(16).encode("hex")
|
||||
|
||||
|
||||
def xsrf_secret_key():
|
||||
"""Return the secret key for use for XSRF protection.
|
||||
|
||||
If the Site entity does not have a secret key, this method will also create
|
||||
one and persist it.
|
||||
|
||||
Returns:
|
||||
The secret key.
|
||||
"""
|
||||
secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
|
||||
if not secret:
|
||||
# Load the one and only instance of SiteXsrfSecretKey.
|
||||
model = SiteXsrfSecretKey.get_or_insert(key_name='site')
|
||||
if not model.secret:
|
||||
model.secret = _generate_new_xsrf_secret_key()
|
||||
model.put()
|
||||
secret = model.secret
|
||||
memcache.add(XSRF_MEMCACHE_ID, secret,
|
||||
namespace=OAUTH2CLIENT_NAMESPACE)
|
||||
|
||||
return str(secret)
|
||||
|
||||
|
||||
class AppAssertionCredentials(client.AssertionCredentials):
|
||||
"""Credentials object for App Engine Assertion Grants
|
||||
|
||||
This object will allow an App Engine application to identify itself to
|
||||
Google and other OAuth 2.0 servers that can verify assertions. It can be
|
||||
used for the purpose of accessing data stored under an account assigned to
|
||||
the App Engine application itself.
|
||||
|
||||
This credential does not require a flow to instantiate because it
|
||||
represents a two legged flow, and therefore has all of the required
|
||||
information to generate and refresh its own access tokens.
|
||||
"""
|
||||
|
||||
@util.positional(2)
|
||||
def __init__(self, scope, **kwargs):
|
||||
"""Constructor for AppAssertionCredentials
|
||||
|
||||
Args:
|
||||
scope: string or iterable of strings, scope(s) of the credentials
|
||||
being requested.
|
||||
**kwargs: optional keyword args, including:
|
||||
service_account_id: service account id of the application. If None
|
||||
or unspecified, the default service account for
|
||||
the app is used.
|
||||
"""
|
||||
self.scope = util.scopes_to_string(scope)
|
||||
self._kwargs = kwargs
|
||||
self.service_account_id = kwargs.get('service_account_id', None)
|
||||
self._service_account_email = None
|
||||
|
||||
# Assertion type is no longer used, but still in the
|
||||
# parent class signature.
|
||||
super(AppAssertionCredentials, self).__init__(None)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
data = json.loads(json_data)
|
||||
return AppAssertionCredentials(data['scope'])
|
||||
|
||||
def _refresh(self, http_request):
|
||||
"""Refreshes the access_token.
|
||||
|
||||
Since the underlying App Engine app_identity implementation does its
|
||||
own caching we can skip all the storage hoops and just to a refresh
|
||||
using the API.
|
||||
|
||||
Args:
|
||||
http_request: callable, a callable that matches the method
|
||||
signature of httplib2.Http.request, used to make the
|
||||
refresh request.
|
||||
|
||||
Raises:
|
||||
AccessTokenRefreshError: When the refresh fails.
|
||||
"""
|
||||
try:
|
||||
scopes = self.scope.split()
|
||||
(token, _) = app_identity.get_access_token(
|
||||
scopes, service_account_id=self.service_account_id)
|
||||
except app_identity.Error as e:
|
||||
raise client.AccessTokenRefreshError(str(e))
|
||||
self.access_token = token
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
raise NotImplementedError('Cannot serialize credentials '
|
||||
'for Google App Engine.')
|
||||
|
||||
def create_scoped_required(self):
|
||||
return not self.scope
|
||||
|
||||
def create_scoped(self, scopes):
|
||||
return AppAssertionCredentials(scopes, **self._kwargs)
|
||||
|
||||
def sign_blob(self, blob):
|
||||
"""Cryptographically sign a blob (of bytes).
|
||||
|
||||
Implements abstract method
|
||||
:meth:`oauth2client.client.AssertionCredentials.sign_blob`.
|
||||
|
||||
Args:
|
||||
blob: bytes, Message to be signed.
|
||||
|
||||
Returns:
|
||||
tuple, A pair of the private key ID used to sign the blob and
|
||||
the signed contents.
|
||||
"""
|
||||
return app_identity.sign_blob(blob)
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
"""Get the email for the current service account.
|
||||
|
||||
Returns:
|
||||
string, The email associated with the Google App Engine
|
||||
service account.
|
||||
"""
|
||||
if self._service_account_email is None:
|
||||
self._service_account_email = (
|
||||
app_identity.get_service_account_name())
|
||||
return self._service_account_email
|
||||
|
||||
|
||||
class FlowProperty(db.Property):
|
||||
"""App Engine datastore Property for Flow.
|
||||
|
||||
Utility property that allows easy storage and retrieval of an
|
||||
oauth2client.Flow
|
||||
"""
|
||||
|
||||
# Tell what the user type is.
|
||||
data_type = client.Flow
|
||||
|
||||
# For writing to datastore.
|
||||
def get_value_for_datastore(self, model_instance):
|
||||
flow = super(FlowProperty, self).get_value_for_datastore(
|
||||
model_instance)
|
||||
return db.Blob(pickle.dumps(flow))
|
||||
|
||||
# For reading from datastore.
|
||||
def make_value_from_datastore(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
return pickle.loads(value)
|
||||
|
||||
def validate(self, value):
|
||||
if value is not None and not isinstance(value, client.Flow):
|
||||
raise db.BadValueError(
|
||||
'Property {0} must be convertible '
|
||||
'to a FlowThreeLegged instance ({1})'.format(self.name, value))
|
||||
return super(FlowProperty, self).validate(value)
|
||||
|
||||
def empty(self, value):
|
||||
return not value
|
||||
|
||||
|
||||
class CredentialsProperty(db.Property):
|
||||
"""App Engine datastore Property for Credentials.
|
||||
|
||||
Utility property that allows easy storage and retrieval of
|
||||
oauth2client.Credentials
|
||||
"""
|
||||
|
||||
# Tell what the user type is.
|
||||
data_type = client.Credentials
|
||||
|
||||
# For writing to datastore.
|
||||
def get_value_for_datastore(self, model_instance):
|
||||
logger.info("get: Got type " + str(type(model_instance)))
|
||||
cred = super(CredentialsProperty, self).get_value_for_datastore(
|
||||
model_instance)
|
||||
if cred is None:
|
||||
cred = ''
|
||||
else:
|
||||
cred = cred.to_json()
|
||||
return db.Blob(cred)
|
||||
|
||||
# For reading from datastore.
|
||||
def make_value_from_datastore(self, value):
|
||||
logger.info("make: Got type " + str(type(value)))
|
||||
if value is None:
|
||||
return None
|
||||
if len(value) == 0:
|
||||
return None
|
||||
try:
|
||||
credentials = client.Credentials.new_from_json(value)
|
||||
except ValueError:
|
||||
credentials = None
|
||||
return credentials
|
||||
|
||||
def validate(self, value):
|
||||
value = super(CredentialsProperty, self).validate(value)
|
||||
logger.info("validate: Got type " + str(type(value)))
|
||||
if value is not None and not isinstance(value, client.Credentials):
|
||||
raise db.BadValueError(
|
||||
'Property {0} must be convertible '
|
||||
'to a Credentials instance ({1})'.format(self.name, value))
|
||||
return value
|
||||
|
||||
|
||||
class StorageByKeyName(client.Storage):
|
||||
"""Store and retrieve a credential to and from the App Engine datastore.
|
||||
|
||||
This Storage helper presumes the Credentials have been stored as a
|
||||
CredentialsProperty or CredentialsNDBProperty on a datastore model class,
|
||||
and that entities are stored by key_name.
|
||||
"""
|
||||
|
||||
@util.positional(4)
|
||||
def __init__(self, model, key_name, property_name, cache=None, user=None):
|
||||
"""Constructor for Storage.
|
||||
|
||||
Args:
|
||||
model: db.Model or ndb.Model, model class
|
||||
key_name: string, key name for the entity that has the credentials
|
||||
property_name: string, name of the property that is a
|
||||
CredentialsProperty or CredentialsNDBProperty.
|
||||
cache: memcache, a write-through cache to put in front of the
|
||||
datastore. If the model you are using is an NDB model, using
|
||||
a cache will be redundant since the model uses an instance
|
||||
cache and memcache for you.
|
||||
user: users.User object, optional. Can be used to grab user ID as a
|
||||
key_name if no key name is specified.
|
||||
"""
|
||||
super(StorageByKeyName, self).__init__()
|
||||
|
||||
if key_name is None:
|
||||
if user is None:
|
||||
raise ValueError('StorageByKeyName called with no '
|
||||
'key name or user.')
|
||||
key_name = user.user_id()
|
||||
|
||||
self._model = model
|
||||
self._key_name = key_name
|
||||
self._property_name = property_name
|
||||
self._cache = cache
|
||||
|
||||
def _is_ndb(self):
|
||||
"""Determine whether the model of the instance is an NDB model.
|
||||
|
||||
Returns:
|
||||
Boolean indicating whether or not the model is an NDB or DB model.
|
||||
"""
|
||||
# issubclass will fail if one of the arguments is not a class, only
|
||||
# need worry about new-style classes since ndb and db models are
|
||||
# new-style
|
||||
if isinstance(self._model, type):
|
||||
if _NDB_MODEL is not None and issubclass(self._model, _NDB_MODEL):
|
||||
return True
|
||||
elif issubclass(self._model, db.Model):
|
||||
return False
|
||||
|
||||
raise TypeError(
|
||||
'Model class not an NDB or DB model: {0}.'.format(self._model))
|
||||
|
||||
def _get_entity(self):
|
||||
"""Retrieve entity from datastore.
|
||||
|
||||
Uses a different model method for db or ndb models.
|
||||
|
||||
Returns:
|
||||
Instance of the model corresponding to the current storage object
|
||||
and stored using the key name of the storage object.
|
||||
"""
|
||||
if self._is_ndb():
|
||||
return self._model.get_by_id(self._key_name)
|
||||
else:
|
||||
return self._model.get_by_key_name(self._key_name)
|
||||
|
||||
def _delete_entity(self):
|
||||
"""Delete entity from datastore.
|
||||
|
||||
Attempts to delete using the key_name stored on the object, whether or
|
||||
not the given key is in the datastore.
|
||||
"""
|
||||
if self._is_ndb():
|
||||
_NDB_KEY(self._model, self._key_name).delete()
|
||||
else:
|
||||
entity_key = db.Key.from_path(self._model.kind(), self._key_name)
|
||||
db.delete(entity_key)
|
||||
|
||||
@db.non_transactional(allow_existing=True)
|
||||
def locked_get(self):
|
||||
"""Retrieve Credential from datastore.
|
||||
|
||||
Returns:
|
||||
oauth2client.Credentials
|
||||
"""
|
||||
credentials = None
|
||||
if self._cache:
|
||||
json = self._cache.get(self._key_name)
|
||||
if json:
|
||||
credentials = client.Credentials.new_from_json(json)
|
||||
if credentials is None:
|
||||
entity = self._get_entity()
|
||||
if entity is not None:
|
||||
credentials = getattr(entity, self._property_name)
|
||||
if self._cache:
|
||||
self._cache.set(self._key_name, credentials.to_json())
|
||||
|
||||
if credentials and hasattr(credentials, 'set_store'):
|
||||
credentials.set_store(self)
|
||||
return credentials
|
||||
|
||||
@db.non_transactional(allow_existing=True)
|
||||
def locked_put(self, credentials):
|
||||
"""Write a Credentials to the datastore.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
entity = self._model.get_or_insert(self._key_name)
|
||||
setattr(entity, self._property_name, credentials)
|
||||
entity.put()
|
||||
if self._cache:
|
||||
self._cache.set(self._key_name, credentials.to_json())
|
||||
|
||||
@db.non_transactional(allow_existing=True)
|
||||
def locked_delete(self):
|
||||
"""Delete Credential from datastore."""
|
||||
|
||||
if self._cache:
|
||||
self._cache.delete(self._key_name)
|
||||
|
||||
self._delete_entity()
|
||||
|
||||
|
||||
class CredentialsModel(db.Model):
|
||||
"""Storage for OAuth 2.0 Credentials
|
||||
|
||||
Storage of the model is keyed by the user.user_id().
|
||||
"""
|
||||
credentials = CredentialsProperty()
|
||||
|
||||
|
||||
def _build_state_value(request_handler, user):
|
||||
"""Composes the value for the 'state' parameter.
|
||||
|
||||
Packs the current request URI and an XSRF token into an opaque string that
|
||||
can be passed to the authentication server via the 'state' parameter.
|
||||
|
||||
Args:
|
||||
request_handler: webapp.RequestHandler, The request.
|
||||
user: google.appengine.api.users.User, The current user.
|
||||
|
||||
Returns:
|
||||
The state value as a string.
|
||||
"""
|
||||
uri = request_handler.request.url
|
||||
token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
|
||||
action_id=str(uri))
|
||||
return uri + ':' + token
|
||||
|
||||
|
||||
def _parse_state_value(state, user):
|
||||
"""Parse the value of the 'state' parameter.
|
||||
|
||||
Parses the value and validates the XSRF token in the state parameter.
|
||||
|
||||
Args:
|
||||
state: string, The value of the state parameter.
|
||||
user: google.appengine.api.users.User, The current user.
|
||||
|
||||
Returns:
|
||||
The redirect URI, or None if XSRF token is not valid.
|
||||
"""
|
||||
uri, token = state.rsplit(':', 1)
|
||||
if xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
|
||||
action_id=uri):
|
||||
return uri
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class OAuth2Decorator(object):
|
||||
"""Utility for making OAuth 2.0 easier.
|
||||
|
||||
Instantiate and then use with oauth_required or oauth_aware
|
||||
as decorators on webapp.RequestHandler methods.
|
||||
|
||||
::
|
||||
|
||||
decorator = OAuth2Decorator(
|
||||
client_id='837...ent.com',
|
||||
client_secret='Qh...wwI',
|
||||
scope='https://www.googleapis.com/auth/plus')
|
||||
|
||||
class MainHandler(webapp.RequestHandler):
|
||||
@decorator.oauth_required
|
||||
def get(self):
|
||||
http = decorator.http()
|
||||
# http is authorized with the user's Credentials and can be
|
||||
# used in API calls
|
||||
|
||||
"""
|
||||
|
||||
def set_credentials(self, credentials):
|
||||
self._tls.credentials = credentials
|
||||
|
||||
def get_credentials(self):
|
||||
"""A thread local Credentials object.
|
||||
|
||||
Returns:
|
||||
A client.Credentials object, or None if credentials hasn't been set
|
||||
in this thread yet, which may happen when calling has_credentials
|
||||
inside oauth_aware.
|
||||
"""
|
||||
return getattr(self._tls, 'credentials', None)
|
||||
|
||||
credentials = property(get_credentials, set_credentials)
|
||||
|
||||
def set_flow(self, flow):
|
||||
self._tls.flow = flow
|
||||
|
||||
def get_flow(self):
|
||||
"""A thread local Flow object.
|
||||
|
||||
Returns:
|
||||
A credentials.Flow object, or None if the flow hasn't been set in
|
||||
this thread yet, which happens in _create_flow() since Flows are
|
||||
created lazily.
|
||||
"""
|
||||
return getattr(self._tls, 'flow', None)
|
||||
|
||||
flow = property(get_flow, set_flow)
|
||||
|
||||
@util.positional(4)
|
||||
def __init__(self, client_id, client_secret, scope,
|
||||
auth_uri=oauth2client.GOOGLE_AUTH_URI,
|
||||
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
|
||||
user_agent=None,
|
||||
message=None,
|
||||
callback_path='/oauth2callback',
|
||||
token_response_param=None,
|
||||
_storage_class=StorageByKeyName,
|
||||
_credentials_class=CredentialsModel,
|
||||
_credentials_property_name='credentials',
|
||||
**kwargs):
|
||||
"""Constructor for OAuth2Decorator
|
||||
|
||||
Args:
|
||||
client_id: string, client identifier.
|
||||
client_secret: string client secret.
|
||||
scope: string or iterable of strings, scope(s) of the credentials
|
||||
being requested.
|
||||
auth_uri: string, URI for authorization endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0 provider
|
||||
can be used.
|
||||
token_uri: string, URI for token endpoint. For convenience defaults
|
||||
to Google's endpoints but any OAuth 2.0 provider can be
|
||||
used.
|
||||
revoke_uri: string, URI for revoke endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0
|
||||
provider can be used.
|
||||
user_agent: string, User agent of your application, default to
|
||||
None.
|
||||
message: Message to display if there are problems with the
|
||||
OAuth 2.0 configuration. The message may contain HTML and
|
||||
will be presented on the web interface for any method that
|
||||
uses the decorator.
|
||||
callback_path: string, The absolute path to use as the callback
|
||||
URI. Note that this must match up with the URI given
|
||||
when registering the application in the APIs
|
||||
Console.
|
||||
token_response_param: string. If provided, the full JSON response
|
||||
to the access token request will be encoded
|
||||
and included in this query parameter in the
|
||||
callback URI. This is useful with providers
|
||||
(e.g. wordpress.com) that include extra
|
||||
fields that the client may want.
|
||||
_storage_class: "Protected" keyword argument not typically provided
|
||||
to this constructor. A storage class to aid in
|
||||
storing a Credentials object for a user in the
|
||||
datastore. Defaults to StorageByKeyName.
|
||||
_credentials_class: "Protected" keyword argument not typically
|
||||
provided to this constructor. A db or ndb Model
|
||||
class to hold credentials. Defaults to
|
||||
CredentialsModel.
|
||||
_credentials_property_name: "Protected" keyword argument not
|
||||
typically provided to this constructor.
|
||||
A string indicating the name of the
|
||||
field on the _credentials_class where a
|
||||
Credentials object will be stored.
|
||||
Defaults to 'credentials'.
|
||||
**kwargs: dict, Keyword arguments are passed along as kwargs to
|
||||
the OAuth2WebServerFlow constructor.
|
||||
"""
|
||||
self._tls = threading.local()
|
||||
self.flow = None
|
||||
self.credentials = None
|
||||
self._client_id = client_id
|
||||
self._client_secret = client_secret
|
||||
self._scope = util.scopes_to_string(scope)
|
||||
self._auth_uri = auth_uri
|
||||
self._token_uri = token_uri
|
||||
self._revoke_uri = revoke_uri
|
||||
self._user_agent = user_agent
|
||||
self._kwargs = kwargs
|
||||
self._message = message
|
||||
self._in_error = False
|
||||
self._callback_path = callback_path
|
||||
self._token_response_param = token_response_param
|
||||
self._storage_class = _storage_class
|
||||
self._credentials_class = _credentials_class
|
||||
self._credentials_property_name = _credentials_property_name
|
||||
|
||||
def _display_error_message(self, request_handler):
|
||||
request_handler.response.out.write('<html><body>')
|
||||
request_handler.response.out.write(_safe_html(self._message))
|
||||
request_handler.response.out.write('</body></html>')
|
||||
|
||||
def oauth_required(self, method):
|
||||
"""Decorator that starts the OAuth 2.0 dance.
|
||||
|
||||
Starts the OAuth dance for the logged in user if they haven't already
|
||||
granted access for this application.
|
||||
|
||||
Args:
|
||||
method: callable, to be decorated method of a webapp.RequestHandler
|
||||
instance.
|
||||
"""
|
||||
|
||||
def check_oauth(request_handler, *args, **kwargs):
|
||||
if self._in_error:
|
||||
self._display_error_message(request_handler)
|
||||
return
|
||||
|
||||
user = users.get_current_user()
|
||||
# Don't use @login_decorator as this could be used in a
|
||||
# POST request.
|
||||
if not user:
|
||||
request_handler.redirect(users.create_login_url(
|
||||
request_handler.request.uri))
|
||||
return
|
||||
|
||||
self._create_flow(request_handler)
|
||||
|
||||
# Store the request URI in 'state' so we can use it later
|
||||
self.flow.params['state'] = _build_state_value(
|
||||
request_handler, user)
|
||||
self.credentials = self._storage_class(
|
||||
self._credentials_class, None,
|
||||
self._credentials_property_name, user=user).get()
|
||||
|
||||
if not self.has_credentials():
|
||||
return request_handler.redirect(self.authorize_url())
|
||||
try:
|
||||
resp = method(request_handler, *args, **kwargs)
|
||||
except client.AccessTokenRefreshError:
|
||||
return request_handler.redirect(self.authorize_url())
|
||||
finally:
|
||||
self.credentials = None
|
||||
return resp
|
||||
|
||||
return check_oauth
|
||||
|
||||
def _create_flow(self, request_handler):
|
||||
"""Create the Flow object.
|
||||
|
||||
The Flow is calculated lazily since we don't know where this app is
|
||||
running until it receives a request, at which point redirect_uri can be
|
||||
calculated and then the Flow object can be constructed.
|
||||
|
||||
Args:
|
||||
request_handler: webapp.RequestHandler, the request handler.
|
||||
"""
|
||||
if self.flow is None:
|
||||
redirect_uri = request_handler.request.relative_url(
|
||||
self._callback_path) # Usually /oauth2callback
|
||||
self.flow = client.OAuth2WebServerFlow(
|
||||
self._client_id, self._client_secret, self._scope,
|
||||
redirect_uri=redirect_uri, user_agent=self._user_agent,
|
||||
auth_uri=self._auth_uri, token_uri=self._token_uri,
|
||||
revoke_uri=self._revoke_uri, **self._kwargs)
|
||||
|
||||
def oauth_aware(self, method):
|
||||
"""Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
|
||||
|
||||
Does all the setup for the OAuth dance, but doesn't initiate it.
|
||||
This decorator is useful if you want to create a page that knows
|
||||
whether or not the user has granted access to this application.
|
||||
From within a method decorated with @oauth_aware the has_credentials()
|
||||
and authorize_url() methods can be called.
|
||||
|
||||
Args:
|
||||
method: callable, to be decorated method of a webapp.RequestHandler
|
||||
instance.
|
||||
"""
|
||||
|
||||
def setup_oauth(request_handler, *args, **kwargs):
|
||||
if self._in_error:
|
||||
self._display_error_message(request_handler)
|
||||
return
|
||||
|
||||
user = users.get_current_user()
|
||||
# Don't use @login_decorator as this could be used in a
|
||||
# POST request.
|
||||
if not user:
|
||||
request_handler.redirect(users.create_login_url(
|
||||
request_handler.request.uri))
|
||||
return
|
||||
|
||||
self._create_flow(request_handler)
|
||||
|
||||
self.flow.params['state'] = _build_state_value(request_handler,
|
||||
user)
|
||||
self.credentials = self._storage_class(
|
||||
self._credentials_class, None,
|
||||
self._credentials_property_name, user=user).get()
|
||||
try:
|
||||
resp = method(request_handler, *args, **kwargs)
|
||||
finally:
|
||||
self.credentials = None
|
||||
return resp
|
||||
return setup_oauth
|
||||
|
||||
def has_credentials(self):
|
||||
"""True if for the logged in user there are valid access Credentials.
|
||||
|
||||
Must only be called from with a webapp.RequestHandler subclassed method
|
||||
that had been decorated with either @oauth_required or @oauth_aware.
|
||||
"""
|
||||
return self.credentials is not None and not self.credentials.invalid
|
||||
|
||||
def authorize_url(self):
|
||||
"""Returns the URL to start the OAuth dance.
|
||||
|
||||
Must only be called from with a webapp.RequestHandler subclassed method
|
||||
that had been decorated with either @oauth_required or @oauth_aware.
|
||||
"""
|
||||
url = self.flow.step1_get_authorize_url()
|
||||
return str(url)
|
||||
|
||||
def http(self, *args, **kwargs):
|
||||
"""Returns an authorized http instance.
|
||||
|
||||
Must only be called from within an @oauth_required decorated method, or
|
||||
from within an @oauth_aware decorated method where has_credentials()
|
||||
returns True.
|
||||
|
||||
Args:
|
||||
*args: Positional arguments passed to httplib2.Http constructor.
|
||||
**kwargs: Positional arguments passed to httplib2.Http constructor.
|
||||
"""
|
||||
return self.credentials.authorize(httplib2.Http(*args, **kwargs))
|
||||
|
||||
@property
|
||||
def callback_path(self):
|
||||
"""The absolute path where the callback will occur.
|
||||
|
||||
Note this is the absolute path, not the absolute URI, that will be
|
||||
calculated by the decorator at runtime. See callback_handler() for how
|
||||
this should be used.
|
||||
|
||||
Returns:
|
||||
The callback path as a string.
|
||||
"""
|
||||
return self._callback_path
|
||||
|
||||
def callback_handler(self):
|
||||
"""RequestHandler for the OAuth 2.0 redirect callback.
|
||||
|
||||
Usage::
|
||||
|
||||
app = webapp.WSGIApplication([
|
||||
('/index', MyIndexHandler),
|
||||
...,
|
||||
(decorator.callback_path, decorator.callback_handler())
|
||||
])
|
||||
|
||||
Returns:
|
||||
A webapp.RequestHandler that handles the redirect back from the
|
||||
server during the OAuth 2.0 dance.
|
||||
"""
|
||||
decorator = self
|
||||
|
||||
class OAuth2Handler(webapp.RequestHandler):
|
||||
"""Handler for the redirect_uri of the OAuth 2.0 dance."""
|
||||
|
||||
@login_required
|
||||
def get(self):
|
||||
error = self.request.get('error')
|
||||
if error:
|
||||
errormsg = self.request.get('error_description', error)
|
||||
self.response.out.write(
|
||||
'The authorization request failed: {0}'.format(
|
||||
_safe_html(errormsg)))
|
||||
else:
|
||||
user = users.get_current_user()
|
||||
decorator._create_flow(self)
|
||||
credentials = decorator.flow.step2_exchange(
|
||||
self.request.params)
|
||||
decorator._storage_class(
|
||||
decorator._credentials_class, None,
|
||||
decorator._credentials_property_name,
|
||||
user=user).put(credentials)
|
||||
redirect_uri = _parse_state_value(
|
||||
str(self.request.get('state')), user)
|
||||
if redirect_uri is None:
|
||||
self.response.out.write(
|
||||
'The authorization request failed')
|
||||
return
|
||||
|
||||
if (decorator._token_response_param and
|
||||
credentials.token_response):
|
||||
resp_json = json.dumps(credentials.token_response)
|
||||
redirect_uri = util._add_query_parameter(
|
||||
redirect_uri, decorator._token_response_param,
|
||||
resp_json)
|
||||
|
||||
self.redirect(redirect_uri)
|
||||
|
||||
return OAuth2Handler
|
||||
|
||||
def callback_application(self):
|
||||
"""WSGI application for handling the OAuth 2.0 redirect callback.
|
||||
|
||||
If you need finer grained control use `callback_handler` which returns
|
||||
just the webapp.RequestHandler.
|
||||
|
||||
Returns:
|
||||
A webapp.WSGIApplication that handles the redirect back from the
|
||||
server during the OAuth 2.0 dance.
|
||||
"""
|
||||
return webapp.WSGIApplication([
|
||||
(self.callback_path, self.callback_handler())
|
||||
])
|
||||
|
||||
|
||||
class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
|
||||
"""An OAuth2Decorator that builds from a clientsecrets file.
|
||||
|
||||
Uses a clientsecrets file as the source for all the information when
|
||||
constructing an OAuth2Decorator.
|
||||
|
||||
::
|
||||
|
||||
decorator = OAuth2DecoratorFromClientSecrets(
|
||||
os.path.join(os.path.dirname(__file__), 'client_secrets.json')
|
||||
scope='https://www.googleapis.com/auth/plus')
|
||||
|
||||
class MainHandler(webapp.RequestHandler):
|
||||
@decorator.oauth_required
|
||||
def get(self):
|
||||
http = decorator.http()
|
||||
# http is authorized with the user's Credentials and can be
|
||||
# used in API calls
|
||||
|
||||
"""
|
||||
|
||||
@util.positional(3)
|
||||
def __init__(self, filename, scope, message=None, cache=None, **kwargs):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
filename: string, File name of client secrets.
|
||||
scope: string or iterable of strings, scope(s) of the credentials
|
||||
being requested.
|
||||
message: string, A friendly string to display to the user if the
|
||||
clientsecrets file is missing or invalid. The message may
|
||||
contain HTML and will be presented on the web interface
|
||||
for any method that uses the decorator.
|
||||
cache: An optional cache service client that implements get() and
|
||||
set()
|
||||
methods. See clientsecrets.loadfile() for details.
|
||||
**kwargs: dict, Keyword arguments are passed along as kwargs to
|
||||
the OAuth2WebServerFlow constructor.
|
||||
"""
|
||||
client_type, client_info = clientsecrets.loadfile(filename,
|
||||
cache=cache)
|
||||
if client_type not in (clientsecrets.TYPE_WEB,
|
||||
clientsecrets.TYPE_INSTALLED):
|
||||
raise clientsecrets.InvalidClientSecretsError(
|
||||
"OAuth2Decorator doesn't support this OAuth 2.0 flow.")
|
||||
|
||||
constructor_kwargs = dict(kwargs)
|
||||
constructor_kwargs.update({
|
||||
'auth_uri': client_info['auth_uri'],
|
||||
'token_uri': client_info['token_uri'],
|
||||
'message': message,
|
||||
})
|
||||
revoke_uri = client_info.get('revoke_uri')
|
||||
if revoke_uri is not None:
|
||||
constructor_kwargs['revoke_uri'] = revoke_uri
|
||||
super(OAuth2DecoratorFromClientSecrets, self).__init__(
|
||||
client_info['client_id'], client_info['client_secret'],
|
||||
scope, **constructor_kwargs)
|
||||
if message is not None:
|
||||
self._message = message
|
||||
else:
|
||||
self._message = 'Please configure your application for OAuth 2.0.'
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def oauth2decorator_from_clientsecrets(filename, scope,
|
||||
message=None, cache=None):
|
||||
"""Creates an OAuth2Decorator populated from a clientsecrets file.
|
||||
|
||||
Args:
|
||||
filename: string, File name of client secrets.
|
||||
scope: string or list of strings, scope(s) of the credentials being
|
||||
requested.
|
||||
message: string, A friendly string to display to the user if the
|
||||
clientsecrets file is missing or invalid. The message may
|
||||
contain HTML and will be presented on the web interface for
|
||||
any method that uses the decorator.
|
||||
cache: An optional cache service client that implements get() and set()
|
||||
methods. See clientsecrets.loadfile() for details.
|
||||
|
||||
Returns: An OAuth2Decorator
|
||||
"""
|
||||
return OAuth2DecoratorFromClientSecrets(filename, scope,
|
||||
message=message, cache=cache)
|
146
venv/Lib/site-packages/oauth2client/contrib/devshell.py
Normal file
146
venv/Lib/site-packages/oauth2client/contrib/devshell.py
Normal file
|
@ -0,0 +1,146 @@
|
|||
# Copyright 2015 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.
|
||||
|
||||
"""OAuth 2.0 utitilies for Google Developer Shell environment."""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
|
||||
from oauth2client import _helpers
|
||||
from oauth2client import client
|
||||
|
||||
DEVSHELL_ENV = 'DEVSHELL_CLIENT_PORT'
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Errors for this module."""
|
||||
pass
|
||||
|
||||
|
||||
class CommunicationError(Error):
|
||||
"""Errors for communication with the Developer Shell server."""
|
||||
|
||||
|
||||
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.
|
||||
CREDENTIAL_INFO_REQUEST_JSON = '[]'
|
||||
|
||||
|
||||
class CredentialInfoResponse(object):
|
||||
"""Credential information response from Developer Shell server.
|
||||
|
||||
The credential information response from Developer Shell socket is a
|
||||
PBLite-formatted JSON array with fields encoded by their index in the
|
||||
array:
|
||||
|
||||
* Index 0 - user email
|
||||
* Index 1 - default project ID. None if the project context is not known.
|
||||
* Index 2 - OAuth2 access token. None if there is no valid auth context.
|
||||
* Index 3 - Seconds until the access token expires. None if not present.
|
||||
"""
|
||||
|
||||
def __init__(self, json_string):
|
||||
"""Initialize the response data from JSON PBLite array."""
|
||||
pbl = json.loads(json_string)
|
||||
if not isinstance(pbl, list):
|
||||
raise ValueError('Not a list: ' + str(pbl))
|
||||
pbl_len = len(pbl)
|
||||
self.user_email = pbl[0] if pbl_len > 0 else None
|
||||
self.project_id = pbl[1] if pbl_len > 1 else None
|
||||
self.access_token = pbl[2] if pbl_len > 2 else None
|
||||
self.expires_in = pbl[3] if pbl_len > 3 else None
|
||||
|
||||
|
||||
def _SendRecv():
|
||||
"""Communicate with the Developer Shell server socket."""
|
||||
|
||||
port = int(os.getenv(DEVSHELL_ENV, 0))
|
||||
if port == 0:
|
||||
raise NoDevshellServer()
|
||||
|
||||
sock = socket.socket()
|
||||
sock.connect(('localhost', port))
|
||||
|
||||
data = CREDENTIAL_INFO_REQUEST_JSON
|
||||
msg = '{0}\n{1}'.format(len(data), data)
|
||||
sock.sendall(_helpers._to_bytes(msg, encoding='utf-8'))
|
||||
|
||||
header = sock.recv(6).decode()
|
||||
if '\n' not in header:
|
||||
raise CommunicationError('saw no newline in the first 6 bytes')
|
||||
len_str, json_str = header.split('\n', 1)
|
||||
to_read = int(len_str) - len(json_str)
|
||||
if to_read > 0:
|
||||
json_str += sock.recv(to_read, socket.MSG_WAITALL).decode()
|
||||
|
||||
return CredentialInfoResponse(json_str)
|
||||
|
||||
|
||||
class DevshellCredentials(client.GoogleCredentials):
|
||||
"""Credentials object for Google Developer Shell environment.
|
||||
|
||||
This object will allow a Google Developer Shell session to identify its
|
||||
user to Google and other OAuth 2.0 servers that can verify assertions. It
|
||||
can be used for the purpose of accessing data stored under the user
|
||||
account.
|
||||
|
||||
This credential does not require a flow to instantiate because it
|
||||
represents a two legged flow, and therefore has all of the required
|
||||
information to generate and refresh its own access tokens.
|
||||
"""
|
||||
|
||||
def __init__(self, user_agent=None):
|
||||
super(DevshellCredentials, self).__init__(
|
||||
None, # access_token, initialized below
|
||||
None, # client_id
|
||||
None, # client_secret
|
||||
None, # refresh_token
|
||||
None, # token_expiry
|
||||
None, # token_uri
|
||||
user_agent)
|
||||
self._refresh(None)
|
||||
|
||||
def _refresh(self, http_request):
|
||||
self.devshell_response = _SendRecv()
|
||||
self.access_token = self.devshell_response.access_token
|
||||
expires_in = self.devshell_response.expires_in
|
||||
if expires_in is not None:
|
||||
delta = datetime.timedelta(seconds=expires_in)
|
||||
self.token_expiry = client._UTCNOW() + delta
|
||||
else:
|
||||
self.token_expiry = None
|
||||
|
||||
@property
|
||||
def user_email(self):
|
||||
return self.devshell_response.user_email
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
return self.devshell_response.project_id
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
raise NotImplementedError(
|
||||
'Cannot load Developer Shell credentials from JSON.')
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
raise NotImplementedError(
|
||||
'Cannot serialize Developer Shell credentials.')
|
|
@ -0,0 +1,65 @@
|
|||
# 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.
|
||||
|
||||
"""Dictionary storage for OAuth2 Credentials."""
|
||||
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
class DictionaryStorage(client.Storage):
|
||||
"""Store and retrieve credentials to and from a dictionary-like object.
|
||||
|
||||
Args:
|
||||
dictionary: A dictionary or dictionary-like object.
|
||||
key: A string or other hashable. The credentials will be stored in
|
||||
``dictionary[key]``.
|
||||
lock: An optional threading.Lock-like object. The lock will be
|
||||
acquired before anything is written or read from the
|
||||
dictionary.
|
||||
"""
|
||||
|
||||
def __init__(self, dictionary, key, lock=None):
|
||||
"""Construct a DictionaryStorage instance."""
|
||||
super(DictionaryStorage, self).__init__(lock=lock)
|
||||
self._dictionary = dictionary
|
||||
self._key = key
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve the credentials from the dictionary, if they exist.
|
||||
|
||||
Returns: A :class:`oauth2client.client.OAuth2Credentials` instance.
|
||||
"""
|
||||
serialized = self._dictionary.get(self._key)
|
||||
|
||||
if serialized is None:
|
||||
return None
|
||||
|
||||
credentials = client.OAuth2Credentials.from_json(serialized)
|
||||
credentials.set_store(self)
|
||||
|
||||
return credentials
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Save the credentials to the dictionary.
|
||||
|
||||
Args:
|
||||
credentials: A :class:`oauth2client.client.OAuth2Credentials`
|
||||
instance.
|
||||
"""
|
||||
serialized = credentials.to_json()
|
||||
self._dictionary[self._key] = serialized
|
||||
|
||||
def locked_delete(self):
|
||||
"""Remove the credentials from the dictionary, if they exist."""
|
||||
self._dictionary.pop(self._key, None)
|
|
@ -0,0 +1,477 @@
|
|||
# Copyright 2015 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.
|
||||
|
||||
"""Utilities for the Django web framework.
|
||||
|
||||
Provides Django views and helpers the make using the OAuth2 web server
|
||||
flow easier. It includes an ``oauth_required`` decorator to automatically
|
||||
ensure that user credentials are available, and an ``oauth_enabled`` decorator
|
||||
to check if the user has authorized, and helper shortcuts to create the
|
||||
authorization URL otherwise.
|
||||
|
||||
There are two basic use cases supported. The first is using Google OAuth as the
|
||||
primary form of authentication, which is the simpler approach recommended
|
||||
for applications without their own user system.
|
||||
|
||||
The second use case is adding Google OAuth credentials to an
|
||||
existing Django model containing a Django user field. Most of the
|
||||
configuration is the same, except for `GOOGLE_OAUTH_MODEL_STORAGE` in
|
||||
settings.py. See "Adding Credentials To An Existing Django User System" for
|
||||
usage differences.
|
||||
|
||||
Only Django versions 1.8+ are supported.
|
||||
|
||||
Configuration
|
||||
===============
|
||||
|
||||
To configure, you'll need a set of OAuth2 web application credentials from
|
||||
`Google Developer's Console <https://console.developers.google.com/project/_/apiui/credential>`.
|
||||
|
||||
Add the helper to your INSTALLED_APPS:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: installed_apps
|
||||
|
||||
INSTALLED_APPS = (
|
||||
# other apps
|
||||
"django.contrib.sessions.middleware"
|
||||
"oauth2client.contrib.django_util"
|
||||
)
|
||||
|
||||
This helper also requires the Django Session Middleware, so
|
||||
``django.contrib.sessions.middleware`` should be in INSTALLED_APPS as well.
|
||||
|
||||
Add the client secrets created earlier to the settings. You can either
|
||||
specify the path to the credentials file in JSON format
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: secrets_file
|
||||
|
||||
GOOGLE_OAUTH2_CLIENT_SECRETS_JSON=/path/to/client-secret.json
|
||||
|
||||
Or, directly configure the client Id and client secret.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: secrets_config
|
||||
|
||||
GOOGLE_OAUTH2_CLIENT_ID=client-id-field
|
||||
GOOGLE_OAUTH2_CLIENT_SECRET=client-secret-field
|
||||
|
||||
By default, the default scopes for the required decorator only contains the
|
||||
``email`` scopes. You can change that default in the settings.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: scopes
|
||||
|
||||
GOOGLE_OAUTH2_SCOPES = ('email', 'https://www.googleapis.com/auth/calendar',)
|
||||
|
||||
By default, the decorators will add an `oauth` object to the Django request
|
||||
object, and include all of its state and helpers inside that object. If the
|
||||
`oauth` name conflicts with another usage, it can be changed
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: request_prefix
|
||||
|
||||
# changes request.oauth to request.google_oauth
|
||||
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'google_oauth'
|
||||
|
||||
Add the oauth2 routes to your application's urls.py urlpatterns.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: urls.py
|
||||
:name: urls
|
||||
|
||||
from oauth2client.contrib.django_util.site import urls as oauth2_urls
|
||||
|
||||
urlpatterns += [url(r'^oauth2/', include(oauth2_urls))]
|
||||
|
||||
To require OAuth2 credentials for a view, use the `oauth2_required` decorator.
|
||||
This creates a credentials object with an id_token, and allows you to create
|
||||
an `http` object to build service clients with. These are all attached to the
|
||||
request.oauth
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_required
|
||||
|
||||
from oauth2client.contrib.django_util.decorators import oauth_required
|
||||
|
||||
@oauth_required
|
||||
def requires_default_scopes(request):
|
||||
email = request.oauth.credentials.id_token['email']
|
||||
service = build(serviceName='calendar', version='v3',
|
||||
http=request.oauth.http,
|
||||
developerKey=API_KEY)
|
||||
events = service.events().list(calendarId='primary').execute()['items']
|
||||
return HttpResponse("email: {0} , calendar: {1}".format(
|
||||
email,str(events)))
|
||||
return HttpResponse(
|
||||
"email: {0} , calendar: {1}".format(email, str(events)))
|
||||
|
||||
To make OAuth2 optional and provide an authorization link in your own views.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_enabled2
|
||||
|
||||
from oauth2client.contrib.django_util.decorators import oauth_enabled
|
||||
|
||||
@oauth_enabled
|
||||
def optional_oauth2(request):
|
||||
if request.oauth.has_credentials():
|
||||
# this could be passed into a view
|
||||
# request.oauth.http is also initialized
|
||||
return HttpResponse("User email: {0}".format(
|
||||
request.oauth.credentials.id_token['email']))
|
||||
else:
|
||||
return HttpResponse(
|
||||
'Here is an OAuth Authorize link: <a href="{0}">Authorize'
|
||||
'</a>'.format(request.oauth.get_authorize_redirect()))
|
||||
|
||||
If a view needs a scope not included in the default scopes specified in
|
||||
the settings, you can use [incremental auth](https://developers.google.com/identity/sign-in/web/incremental-auth)
|
||||
and specify additional scopes in the decorator arguments.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_required_additional_scopes
|
||||
|
||||
@oauth_enabled(scopes=['https://www.googleapis.com/auth/drive'])
|
||||
def drive_required(request):
|
||||
if request.oauth.has_credentials():
|
||||
service = build(serviceName='drive', version='v2',
|
||||
http=request.oauth.http,
|
||||
developerKey=API_KEY)
|
||||
events = service.files().list().execute()['items']
|
||||
return HttpResponse(str(events))
|
||||
else:
|
||||
return HttpResponse(
|
||||
'Here is an OAuth Authorize link: <a href="{0}">Authorize'
|
||||
'</a>'.format(request.oauth.get_authorize_redirect()))
|
||||
|
||||
|
||||
To provide a callback on authorization being completed, use the
|
||||
oauth2_authorized signal:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: signals
|
||||
|
||||
from oauth2client.contrib.django_util.signals import oauth2_authorized
|
||||
|
||||
def test_callback(sender, request, credentials, **kwargs):
|
||||
print("Authorization Signal Received {0}".format(
|
||||
credentials.id_token['email']))
|
||||
|
||||
oauth2_authorized.connect(test_callback)
|
||||
|
||||
Adding Credentials To An Existing Django User System
|
||||
=====================================================
|
||||
|
||||
As an alternative to storing the credentials in the session, the helper
|
||||
can be configured to store the fields on a Django model. This might be useful
|
||||
if you need to use the credentials outside the context of a user request. It
|
||||
also prevents the need for a logged in user to repeat the OAuth flow when
|
||||
starting a new session.
|
||||
|
||||
To use, change ``settings.py``
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: storage_model_config
|
||||
|
||||
GOOGLE_OAUTH2_STORAGE_MODEL = {
|
||||
'model': 'path.to.model.MyModel',
|
||||
'user_property': 'user_id',
|
||||
'credentials_property': 'credential'
|
||||
}
|
||||
|
||||
Where ``path.to.model`` class is the fully qualified name of a
|
||||
``django.db.model`` class containing a ``django.contrib.auth.models.User``
|
||||
field with the name specified by `user_property` and a
|
||||
:class:`oauth2client.contrib.django_util.models.CredentialsField` with the name
|
||||
specified by `credentials_property`. For the sample configuration given,
|
||||
our model would look like
|
||||
|
||||
.. code-block:: python
|
||||
:caption: models.py
|
||||
:name: storage_model_model
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from oauth2client.contrib.django_util.models import CredentialsField
|
||||
|
||||
class MyModel(models.Model):
|
||||
# ... other fields here ...
|
||||
user = models.OneToOneField(User)
|
||||
credential = CredentialsField()
|
||||
"""
|
||||
|
||||
import importlib
|
||||
|
||||
import django.conf
|
||||
from django.core import exceptions
|
||||
from django.core import urlresolvers
|
||||
import httplib2
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from oauth2client import clientsecrets
|
||||
from oauth2client.contrib import dictionary_storage
|
||||
from oauth2client.contrib.django_util import storage
|
||||
|
||||
GOOGLE_OAUTH2_DEFAULT_SCOPES = ('email',)
|
||||
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'oauth'
|
||||
|
||||
|
||||
def _load_client_secrets(filename):
|
||||
"""Loads client secrets from the given filename.
|
||||
|
||||
Args:
|
||||
filename: The name of the file containing the JSON secret key.
|
||||
|
||||
Returns:
|
||||
A 2-tuple, the first item containing the client id, and the second
|
||||
item containing a client secret.
|
||||
"""
|
||||
client_type, client_info = clientsecrets.loadfile(filename)
|
||||
|
||||
if client_type != clientsecrets.TYPE_WEB:
|
||||
raise ValueError(
|
||||
'The flow specified in {} is not supported, only the WEB flow '
|
||||
'type is supported.'.format(client_type))
|
||||
return client_info['client_id'], client_info['client_secret']
|
||||
|
||||
|
||||
def _get_oauth2_client_id_and_secret(settings_instance):
|
||||
"""Initializes client id and client secret based on the settings.
|
||||
|
||||
Args:
|
||||
settings_instance: An instance of ``django.conf.settings``.
|
||||
|
||||
Returns:
|
||||
A 2-tuple, the first item is the client id and the second
|
||||
item is the client secret.
|
||||
"""
|
||||
secret_json = getattr(settings_instance,
|
||||
'GOOGLE_OAUTH2_CLIENT_SECRETS_JSON', None)
|
||||
if secret_json is not None:
|
||||
return _load_client_secrets(secret_json)
|
||||
else:
|
||||
client_id = getattr(settings_instance, "GOOGLE_OAUTH2_CLIENT_ID",
|
||||
None)
|
||||
client_secret = getattr(settings_instance,
|
||||
"GOOGLE_OAUTH2_CLIENT_SECRET", None)
|
||||
if client_id is not None and client_secret is not None:
|
||||
return client_id, client_secret
|
||||
else:
|
||||
raise exceptions.ImproperlyConfigured(
|
||||
"Must specify either GOOGLE_OAUTH2_CLIENT_SECRETS_JSON, or "
|
||||
"both GOOGLE_OAUTH2_CLIENT_ID and "
|
||||
"GOOGLE_OAUTH2_CLIENT_SECRET in settings.py")
|
||||
|
||||
|
||||
def _get_storage_model():
|
||||
"""This configures whether the credentials will be stored in the session
|
||||
or the Django ORM based on the settings. By default, the credentials
|
||||
will be stored in the session, unless `GOOGLE_OAUTH2_STORAGE_MODEL`
|
||||
is found in the settings. Usually, the ORM storage is used to integrate
|
||||
credentials into an existing Django user system.
|
||||
|
||||
Returns:
|
||||
A tuple containing three strings, or None. If
|
||||
``GOOGLE_OAUTH2_STORAGE_MODEL`` is configured, the tuple
|
||||
will contain the fully qualifed path of the `django.db.model`,
|
||||
the name of the ``django.contrib.auth.models.User`` field on the
|
||||
model, and the name of the
|
||||
:class:`oauth2client.contrib.django_util.models.CredentialsField`
|
||||
field on the model. If Django ORM storage is not configured,
|
||||
this function returns None.
|
||||
"""
|
||||
storage_model_settings = getattr(django.conf.settings,
|
||||
'GOOGLE_OAUTH2_STORAGE_MODEL', None)
|
||||
if storage_model_settings is not None:
|
||||
return (storage_model_settings['model'],
|
||||
storage_model_settings['user_property'],
|
||||
storage_model_settings['credentials_property'])
|
||||
else:
|
||||
return None, None, None
|
||||
|
||||
|
||||
class OAuth2Settings(object):
|
||||
"""Initializes Django OAuth2 Helper Settings
|
||||
|
||||
This class loads the OAuth2 Settings from the Django settings, and then
|
||||
provides those settings as attributes to the rest of the views and
|
||||
decorators in the module.
|
||||
|
||||
Attributes:
|
||||
scopes: A list of OAuth2 scopes that the decorators and views will use
|
||||
as defaults.
|
||||
request_prefix: The name of the attribute that the decorators use to
|
||||
attach the UserOAuth2 object to the Django request object.
|
||||
client_id: The OAuth2 Client ID.
|
||||
client_secret: The OAuth2 Client Secret.
|
||||
"""
|
||||
|
||||
def __init__(self, settings_instance):
|
||||
self.scopes = getattr(settings_instance, 'GOOGLE_OAUTH2_SCOPES',
|
||||
GOOGLE_OAUTH2_DEFAULT_SCOPES)
|
||||
self.request_prefix = getattr(settings_instance,
|
||||
'GOOGLE_OAUTH2_REQUEST_ATTRIBUTE',
|
||||
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE)
|
||||
self.client_id, self.client_secret = \
|
||||
_get_oauth2_client_id_and_secret(settings_instance)
|
||||
|
||||
if ('django.contrib.sessions.middleware.SessionMiddleware'
|
||||
not in settings_instance.MIDDLEWARE_CLASSES):
|
||||
raise exceptions.ImproperlyConfigured(
|
||||
'The Google OAuth2 Helper requires session middleware to '
|
||||
'be installed. Edit your MIDDLEWARE_CLASSES setting'
|
||||
' to include \'django.contrib.sessions.middleware.'
|
||||
'SessionMiddleware\'.')
|
||||
(self.storage_model, self.storage_model_user_property,
|
||||
self.storage_model_credentials_property) = _get_storage_model()
|
||||
|
||||
|
||||
oauth2_settings = OAuth2Settings(django.conf.settings)
|
||||
|
||||
_CREDENTIALS_KEY = 'google_oauth2_credentials'
|
||||
|
||||
|
||||
def get_storage(request):
|
||||
""" Gets a Credentials storage object provided by the Django OAuth2 Helper
|
||||
object.
|
||||
|
||||
Args:
|
||||
request: Reference to the current request object.
|
||||
|
||||
Returns:
|
||||
An :class:`oauth2.client.Storage` object.
|
||||
"""
|
||||
storage_model = oauth2_settings.storage_model
|
||||
user_property = oauth2_settings.storage_model_user_property
|
||||
credentials_property = oauth2_settings.storage_model_credentials_property
|
||||
|
||||
if storage_model:
|
||||
module_name, class_name = storage_model.rsplit('.', 1)
|
||||
module = importlib.import_module(module_name)
|
||||
storage_model_class = getattr(module, class_name)
|
||||
return storage.DjangoORMStorage(storage_model_class,
|
||||
user_property,
|
||||
request.user,
|
||||
credentials_property)
|
||||
else:
|
||||
# use session
|
||||
return dictionary_storage.DictionaryStorage(
|
||||
request.session, key=_CREDENTIALS_KEY)
|
||||
|
||||
|
||||
def _redirect_with_params(url_name, *args, **kwargs):
|
||||
"""Helper method to create a redirect response with URL params.
|
||||
|
||||
This builds a redirect string that converts kwargs into a
|
||||
query string.
|
||||
|
||||
Args:
|
||||
url_name: The name of the url to redirect to.
|
||||
kwargs: the query string param and their values to build.
|
||||
|
||||
Returns:
|
||||
A properly formatted redirect string.
|
||||
"""
|
||||
url = urlresolvers.reverse(url_name, args=args)
|
||||
params = parse.urlencode(kwargs, True)
|
||||
return "{0}?{1}".format(url, params)
|
||||
|
||||
|
||||
def _credentials_from_request(request):
|
||||
"""Gets the authorized credentials for this flow, if they exist."""
|
||||
# ORM storage requires a logged in user
|
||||
if (oauth2_settings.storage_model is None or
|
||||
request.user.is_authenticated()):
|
||||
return get_storage(request).get()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class UserOAuth2(object):
|
||||
"""Class to create oauth2 objects on Django request objects containing
|
||||
credentials and helper methods.
|
||||
"""
|
||||
|
||||
def __init__(self, request, scopes=None, return_url=None):
|
||||
"""Initialize the Oauth2 Object.
|
||||
|
||||
Args:
|
||||
request: Django request object.
|
||||
scopes: Scopes desired for this OAuth2 flow.
|
||||
return_url: The url to return to after the OAuth flow is complete,
|
||||
defaults to the request's current URL path.
|
||||
"""
|
||||
self.request = request
|
||||
self.return_url = return_url or request.get_full_path()
|
||||
if scopes:
|
||||
self._scopes = set(oauth2_settings.scopes) | set(scopes)
|
||||
else:
|
||||
self._scopes = set(oauth2_settings.scopes)
|
||||
|
||||
def get_authorize_redirect(self):
|
||||
"""Creates a URl to start the OAuth2 authorization flow."""
|
||||
get_params = {
|
||||
'return_url': self.return_url,
|
||||
'scopes': self._get_scopes()
|
||||
}
|
||||
|
||||
return _redirect_with_params('google_oauth:authorize', **get_params)
|
||||
|
||||
def has_credentials(self):
|
||||
"""Returns True if there are valid credentials for the current user
|
||||
and required scopes."""
|
||||
credentials = _credentials_from_request(self.request)
|
||||
return (credentials and not credentials.invalid and
|
||||
credentials.has_scopes(self._get_scopes()))
|
||||
|
||||
def _get_scopes(self):
|
||||
"""Returns the scopes associated with this object, kept up to
|
||||
date for incremental auth."""
|
||||
if _credentials_from_request(self.request):
|
||||
return (self._scopes |
|
||||
_credentials_from_request(self.request).scopes)
|
||||
else:
|
||||
return self._scopes
|
||||
|
||||
@property
|
||||
def scopes(self):
|
||||
"""Returns the scopes associated with this OAuth2 object."""
|
||||
# make sure previously requested custom scopes are maintained
|
||||
# in future authorizations
|
||||
return self._get_scopes()
|
||||
|
||||
@property
|
||||
def credentials(self):
|
||||
"""Gets the authorized credentials for this flow, if they exist."""
|
||||
return _credentials_from_request(self.request)
|
||||
|
||||
@property
|
||||
def http(self):
|
||||
"""Helper method to create an HTTP client authorized with OAuth2
|
||||
credentials."""
|
||||
if self.has_credentials():
|
||||
return self.credentials.authorize(httplib2.Http())
|
||||
return None
|
|
@ -0,0 +1,32 @@
|
|||
# Copyright 2015 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.
|
||||
|
||||
"""Application Config For Django OAuth2 Helper.
|
||||
|
||||
Django 1.7+ provides an
|
||||
[applications](https://docs.djangoproject.com/en/1.8/ref/applications/)
|
||||
API so that Django projects can introspect on installed applications using a
|
||||
stable API. This module exists to follow that convention.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
# Django 1.7+ only supports Python 2.7+
|
||||
if sys.hexversion >= 0x02070000: # pragma: NO COVER
|
||||
from django.apps import AppConfig
|
||||
|
||||
class GoogleOAuth2HelperConfig(AppConfig):
|
||||
""" App Config for Django Helper"""
|
||||
name = 'oauth2client.django_util'
|
||||
verbose_name = "Google OAuth2 Django Helper"
|
|
@ -0,0 +1,145 @@
|
|||
# Copyright 2015 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.
|
||||
|
||||
"""Decorators for Django OAuth2 Flow.
|
||||
|
||||
Contains two decorators, ``oauth_required`` and ``oauth_enabled``.
|
||||
|
||||
``oauth_required`` will ensure that a user has an oauth object containing
|
||||
credentials associated with the request, and if not, redirect to the
|
||||
authorization flow.
|
||||
|
||||
``oauth_enabled`` will attach the oauth2 object containing credentials if it
|
||||
exists. If it doesn't, the view will still render, but helper methods will be
|
||||
attached to start the oauth2 flow.
|
||||
"""
|
||||
|
||||
from django import shortcuts
|
||||
import django.conf
|
||||
from six import wraps
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from oauth2client.contrib import django_util
|
||||
|
||||
|
||||
def oauth_required(decorated_function=None, scopes=None, **decorator_kwargs):
|
||||
""" Decorator to require OAuth2 credentials for a view.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_required_2
|
||||
|
||||
|
||||
from oauth2client.django_util.decorators import oauth_required
|
||||
|
||||
@oauth_required
|
||||
def requires_default_scopes(request):
|
||||
email = request.credentials.id_token['email']
|
||||
service = build(serviceName='calendar', version='v3',
|
||||
http=request.oauth.http,
|
||||
developerKey=API_KEY)
|
||||
events = service.events().list(
|
||||
calendarId='primary').execute()['items']
|
||||
return HttpResponse(
|
||||
"email: {0}, calendar: {1}".format(email, str(events)))
|
||||
|
||||
Args:
|
||||
decorated_function: View function to decorate, must have the Django
|
||||
request object as the first argument.
|
||||
scopes: Scopes to require, will default.
|
||||
decorator_kwargs: Can include ``return_url`` to specify the URL to
|
||||
return to after OAuth2 authorization is complete.
|
||||
|
||||
Returns:
|
||||
An OAuth2 Authorize view if credentials are not found or if the
|
||||
credentials are missing the required scopes. Otherwise,
|
||||
the decorated view.
|
||||
"""
|
||||
def curry_wrapper(wrapped_function):
|
||||
@wraps(wrapped_function)
|
||||
def required_wrapper(request, *args, **kwargs):
|
||||
if not (django_util.oauth2_settings.storage_model is None or
|
||||
request.user.is_authenticated()):
|
||||
redirect_str = '{0}?next={1}'.format(
|
||||
django.conf.settings.LOGIN_URL,
|
||||
parse.quote(request.path))
|
||||
return shortcuts.redirect(redirect_str)
|
||||
|
||||
return_url = decorator_kwargs.pop('return_url',
|
||||
request.get_full_path())
|
||||
user_oauth = django_util.UserOAuth2(request, scopes, return_url)
|
||||
if not user_oauth.has_credentials():
|
||||
return shortcuts.redirect(user_oauth.get_authorize_redirect())
|
||||
setattr(request, django_util.oauth2_settings.request_prefix,
|
||||
user_oauth)
|
||||
return wrapped_function(request, *args, **kwargs)
|
||||
|
||||
return required_wrapper
|
||||
|
||||
if decorated_function:
|
||||
return curry_wrapper(decorated_function)
|
||||
else:
|
||||
return curry_wrapper
|
||||
|
||||
|
||||
def oauth_enabled(decorated_function=None, scopes=None, **decorator_kwargs):
|
||||
""" Decorator to enable OAuth Credentials if authorized, and setup
|
||||
the oauth object on the request object to provide helper functions
|
||||
to start the flow otherwise.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_enabled3
|
||||
|
||||
from oauth2client.django_util.decorators import oauth_enabled
|
||||
|
||||
@oauth_enabled
|
||||
def optional_oauth2(request):
|
||||
if request.oauth.has_credentials():
|
||||
# this could be passed into a view
|
||||
# request.oauth.http is also initialized
|
||||
return HttpResponse("User email: {0}".format(
|
||||
request.oauth.credentials.id_token['email'])
|
||||
else:
|
||||
return HttpResponse('Here is an OAuth Authorize link:
|
||||
<a href="{0}">Authorize</a>'.format(
|
||||
request.oauth.get_authorize_redirect()))
|
||||
|
||||
|
||||
Args:
|
||||
decorated_function: View function to decorate.
|
||||
scopes: Scopes to require, will default.
|
||||
decorator_kwargs: Can include ``return_url`` to specify the URL to
|
||||
return to after OAuth2 authorization is complete.
|
||||
|
||||
Returns:
|
||||
The decorated view function.
|
||||
"""
|
||||
def curry_wrapper(wrapped_function):
|
||||
@wraps(wrapped_function)
|
||||
def enabled_wrapper(request, *args, **kwargs):
|
||||
return_url = decorator_kwargs.pop('return_url',
|
||||
request.get_full_path())
|
||||
user_oauth = django_util.UserOAuth2(request, scopes, return_url)
|
||||
setattr(request, django_util.oauth2_settings.request_prefix,
|
||||
user_oauth)
|
||||
return wrapped_function(request, *args, **kwargs)
|
||||
|
||||
return enabled_wrapper
|
||||
|
||||
if decorated_function:
|
||||
return curry_wrapper(decorated_function)
|
||||
else:
|
||||
return curry_wrapper
|
|
@ -0,0 +1,75 @@
|
|||
# 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.
|
||||
|
||||
"""Contains classes used for the Django ORM storage."""
|
||||
|
||||
import base64
|
||||
import pickle
|
||||
|
||||
from django.db import models
|
||||
from django.utils import encoding
|
||||
|
||||
import oauth2client
|
||||
|
||||
|
||||
class CredentialsField(models.Field):
|
||||
"""Django ORM field for storing OAuth2 Credentials."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'null' not in kwargs:
|
||||
kwargs['null'] = True
|
||||
super(CredentialsField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return 'BinaryField'
|
||||
|
||||
def from_db_value(self, value, expression, connection, context):
|
||||
"""Overrides ``models.Field`` method. This converts the value
|
||||
returned from the database to an instance of this class.
|
||||
"""
|
||||
return self.to_python(value)
|
||||
|
||||
def to_python(self, value):
|
||||
"""Overrides ``models.Field`` method. This is used to convert
|
||||
bytes (from serialization etc) to an instance of this class"""
|
||||
if value is None:
|
||||
return None
|
||||
elif isinstance(value, oauth2client.client.Credentials):
|
||||
return value
|
||||
else:
|
||||
return pickle.loads(base64.b64decode(encoding.smart_bytes(value)))
|
||||
|
||||
def get_prep_value(self, value):
|
||||
"""Overrides ``models.Field`` method. This is used to convert
|
||||
the value from an instances of this class to bytes that can be
|
||||
inserted into the database.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
else:
|
||||
return encoding.smart_text(base64.b64encode(pickle.dumps(value)))
|
||||
|
||||
def value_to_string(self, obj):
|
||||
"""Convert the field value from the provided model to a string.
|
||||
|
||||
Used during model serialization.
|
||||
|
||||
Args:
|
||||
obj: db.Model, model object
|
||||
|
||||
Returns:
|
||||
string, the serialized field value
|
||||
"""
|
||||
value = self._get_val_from_obj(obj)
|
||||
return self.get_prep_value(value)
|
|
@ -0,0 +1,28 @@
|
|||
# Copyright 2015 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.
|
||||
|
||||
"""Signals for Google OAuth2 Helper.
|
||||
|
||||
This module contains signals for Google OAuth2 Helper. Currently it only
|
||||
contains one, which fires when an OAuth2 authorization flow has completed.
|
||||
"""
|
||||
|
||||
import django.dispatch
|
||||
|
||||
"""Signal that fires when OAuth2 Flow has completed.
|
||||
It passes the Django request object and the OAuth2 credentials object to the
|
||||
receiver.
|
||||
"""
|
||||
oauth2_authorized = django.dispatch.Signal(
|
||||
providing_args=["request", "credentials"])
|
|
@ -0,0 +1,26 @@
|
|||
# Copyright 2015 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.
|
||||
|
||||
"""Contains Django URL patterns used for OAuth2 flow."""
|
||||
|
||||
from django.conf import urls
|
||||
|
||||
from oauth2client.contrib.django_util import views
|
||||
|
||||
urlpatterns = [
|
||||
urls.url(r'oauth2callback/', views.oauth2_callback, name="callback"),
|
||||
urls.url(r'oauth2authorize/', views.oauth2_authorize, name="authorize")
|
||||
]
|
||||
|
||||
urls = (urlpatterns, "google_oauth", "google_oauth")
|
|
@ -0,0 +1,81 @@
|
|||
# Copyright 2015 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.
|
||||
|
||||
"""Contains a storage module that stores credentials using the Django ORM."""
|
||||
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
class DjangoORMStorage(client.Storage):
|
||||
"""Store and retrieve a single credential to and from the Django datastore.
|
||||
|
||||
This Storage helper presumes the Credentials
|
||||
have been stored as a CredentialsField
|
||||
on a db model class.
|
||||
"""
|
||||
|
||||
def __init__(self, model_class, key_name, key_value, property_name):
|
||||
"""Constructor for Storage.
|
||||
|
||||
Args:
|
||||
model: string, fully qualified name of db.Model model class.
|
||||
key_name: string, key name for the entity that has the credentials
|
||||
key_value: string, key value for the entity that has the
|
||||
credentials.
|
||||
property_name: string, name of the property that is an
|
||||
CredentialsProperty.
|
||||
"""
|
||||
super(DjangoORMStorage, self).__init__()
|
||||
self.model_class = model_class
|
||||
self.key_name = key_name
|
||||
self.key_value = key_value
|
||||
self.property_name = property_name
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve stored credential from the Django ORM.
|
||||
|
||||
Returns:
|
||||
oauth2client.Credentials retrieved from the Django ORM, associated
|
||||
with the ``model``, ``key_value``->``key_name`` pair used to query
|
||||
for the model, and ``property_name`` identifying the
|
||||
``CredentialsProperty`` field, all of which are defined in the
|
||||
constructor for this Storage object.
|
||||
|
||||
"""
|
||||
query = {self.key_name: self.key_value}
|
||||
entities = self.model_class.objects.filter(**query)
|
||||
if len(entities) > 0:
|
||||
credential = getattr(entities[0], self.property_name)
|
||||
if getattr(credential, 'set_store', None) is not None:
|
||||
credential.set_store(self)
|
||||
return credential
|
||||
else:
|
||||
return None
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Write a Credentials to the Django datastore.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
entity, _ = self.model_class.objects.get_or_create(
|
||||
**{self.key_name: self.key_value})
|
||||
|
||||
setattr(entity, self.property_name, credentials)
|
||||
entity.save()
|
||||
|
||||
def locked_delete(self):
|
||||
"""Delete Credentials from the datastore."""
|
||||
query = {self.key_name: self.key_value}
|
||||
self.model_class.objects.filter(**query).delete()
|
190
venv/Lib/site-packages/oauth2client/contrib/django_util/views.py
Normal file
190
venv/Lib/site-packages/oauth2client/contrib/django_util/views.py
Normal file
|
@ -0,0 +1,190 @@
|
|||
# Copyright 2015 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.
|
||||
|
||||
"""This module contains the views used by the OAuth2 flows.
|
||||
|
||||
Their are two views used by the OAuth2 flow, the authorize and the callback
|
||||
view. The authorize view kicks off the three-legged OAuth flow, and the
|
||||
callback view validates the flow and if successful stores the credentials
|
||||
in the configured storage."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
|
||||
from django import http
|
||||
from django import shortcuts
|
||||
from django.conf import settings
|
||||
from django.core import urlresolvers
|
||||
from django.shortcuts import redirect
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from oauth2client import client
|
||||
from oauth2client.contrib import django_util
|
||||
from oauth2client.contrib.django_util import get_storage
|
||||
from oauth2client.contrib.django_util import signals
|
||||
|
||||
_CSRF_KEY = 'google_oauth2_csrf_token'
|
||||
_FLOW_KEY = 'google_oauth2_flow_{0}'
|
||||
|
||||
|
||||
def _make_flow(request, scopes, return_url=None):
|
||||
"""Creates a Web Server Flow
|
||||
|
||||
Args:
|
||||
request: A Django request object.
|
||||
scopes: the request oauth2 scopes.
|
||||
return_url: The URL to return to after the flow is complete. Defaults
|
||||
to the path of the current request.
|
||||
|
||||
Returns:
|
||||
An OAuth2 flow object that has been stored in the session.
|
||||
"""
|
||||
# Generate a CSRF token to prevent malicious requests.
|
||||
csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
|
||||
|
||||
request.session[_CSRF_KEY] = csrf_token
|
||||
|
||||
state = json.dumps({
|
||||
'csrf_token': csrf_token,
|
||||
'return_url': return_url,
|
||||
})
|
||||
|
||||
flow = client.OAuth2WebServerFlow(
|
||||
client_id=django_util.oauth2_settings.client_id,
|
||||
client_secret=django_util.oauth2_settings.client_secret,
|
||||
scope=scopes,
|
||||
state=state,
|
||||
redirect_uri=request.build_absolute_uri(
|
||||
urlresolvers.reverse("google_oauth:callback")))
|
||||
|
||||
flow_key = _FLOW_KEY.format(csrf_token)
|
||||
request.session[flow_key] = pickle.dumps(flow)
|
||||
return flow
|
||||
|
||||
|
||||
def _get_flow_for_token(csrf_token, request):
|
||||
""" Looks up the flow in session to recover information about requested
|
||||
scopes.
|
||||
|
||||
Args:
|
||||
csrf_token: The token passed in the callback request that should
|
||||
match the one previously generated and stored in the request on the
|
||||
initial authorization view.
|
||||
|
||||
Returns:
|
||||
The OAuth2 Flow object associated with this flow based on the
|
||||
CSRF token.
|
||||
"""
|
||||
flow_pickle = request.session.get(_FLOW_KEY.format(csrf_token), None)
|
||||
return None if flow_pickle is None else pickle.loads(flow_pickle)
|
||||
|
||||
|
||||
def oauth2_callback(request):
|
||||
""" View that handles the user's return from OAuth2 provider.
|
||||
|
||||
This view verifies the CSRF state and OAuth authorization code, and on
|
||||
success stores the credentials obtained in the storage provider,
|
||||
and redirects to the return_url specified in the authorize view and
|
||||
stored in the session.
|
||||
|
||||
Args:
|
||||
request: Django request.
|
||||
|
||||
Returns:
|
||||
A redirect response back to the return_url.
|
||||
"""
|
||||
if 'error' in request.GET:
|
||||
reason = request.GET.get(
|
||||
'error_description', request.GET.get('error', ''))
|
||||
return http.HttpResponseBadRequest(
|
||||
'Authorization failed {0}'.format(reason))
|
||||
|
||||
try:
|
||||
encoded_state = request.GET['state']
|
||||
code = request.GET['code']
|
||||
except KeyError:
|
||||
return http.HttpResponseBadRequest(
|
||||
'Request missing state or authorization code')
|
||||
|
||||
try:
|
||||
server_csrf = request.session[_CSRF_KEY]
|
||||
except KeyError:
|
||||
return http.HttpResponseBadRequest(
|
||||
'No existing session for this flow.')
|
||||
|
||||
try:
|
||||
state = json.loads(encoded_state)
|
||||
client_csrf = state['csrf_token']
|
||||
return_url = state['return_url']
|
||||
except (ValueError, KeyError):
|
||||
return http.HttpResponseBadRequest('Invalid state parameter.')
|
||||
|
||||
if client_csrf != server_csrf:
|
||||
return http.HttpResponseBadRequest('Invalid CSRF token.')
|
||||
|
||||
flow = _get_flow_for_token(client_csrf, request)
|
||||
|
||||
if not flow:
|
||||
return http.HttpResponseBadRequest('Missing Oauth2 flow.')
|
||||
|
||||
try:
|
||||
credentials = flow.step2_exchange(code)
|
||||
except client.FlowExchangeError as exchange_error:
|
||||
return http.HttpResponseBadRequest(
|
||||
'An error has occurred: {0}'.format(exchange_error))
|
||||
|
||||
get_storage(request).put(credentials)
|
||||
|
||||
signals.oauth2_authorized.send(sender=signals.oauth2_authorized,
|
||||
request=request, credentials=credentials)
|
||||
|
||||
return shortcuts.redirect(return_url)
|
||||
|
||||
|
||||
def oauth2_authorize(request):
|
||||
""" View to start the OAuth2 Authorization flow.
|
||||
|
||||
This view starts the OAuth2 authorization flow. If scopes is passed in
|
||||
as a GET URL parameter, it will authorize those scopes, otherwise the
|
||||
default scopes specified in settings. The return_url can also be
|
||||
specified as a GET parameter, otherwise the referer header will be
|
||||
checked, and if that isn't found it will return to the root path.
|
||||
|
||||
Args:
|
||||
request: The Django request object.
|
||||
|
||||
Returns:
|
||||
A redirect to Google OAuth2 Authorization.
|
||||
"""
|
||||
return_url = request.GET.get('return_url', None)
|
||||
|
||||
# Model storage (but not session storage) requires a logged in user
|
||||
if django_util.oauth2_settings.storage_model:
|
||||
if not request.user.is_authenticated():
|
||||
return redirect('{0}?next={1}'.format(
|
||||
settings.LOGIN_URL, parse.quote(request.get_full_path())))
|
||||
# This checks for the case where we ended up here because of a logged
|
||||
# out user but we had credentials for it in the first place
|
||||
elif get_storage(request).get() is not None:
|
||||
return redirect(return_url)
|
||||
|
||||
scopes = request.GET.getlist('scopes', django_util.oauth2_settings.scopes)
|
||||
|
||||
if not return_url:
|
||||
return_url = request.META.get('HTTP_REFERER', '/')
|
||||
flow = _make_flow(request=request, scopes=scopes, return_url=return_url)
|
||||
auth_url = flow.step1_get_authorize_url()
|
||||
return shortcuts.redirect(auth_url)
|
556
venv/Lib/site-packages/oauth2client/contrib/flask_util.py
Normal file
556
venv/Lib/site-packages/oauth2client/contrib/flask_util.py
Normal file
|
@ -0,0 +1,556 @@
|
|||
# Copyright 2015 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.
|
||||
|
||||
"""Utilities for the Flask web framework
|
||||
|
||||
Provides a Flask extension that makes using OAuth2 web server flow easier.
|
||||
The extension includes views that handle the entire auth flow and a
|
||||
``@required`` decorator to automatically ensure that user credentials are
|
||||
available.
|
||||
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
To configure, you'll need a set of OAuth2 web application credentials from the
|
||||
`Google Developer's Console <https://console.developers.google.com/project/_/\
|
||||
apiui/credential>`__.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from oauth2client.contrib.flask_util import UserOAuth2
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
app.config['SECRET_KEY'] = 'your-secret-key'
|
||||
|
||||
app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE'] = 'client_secrets.json'
|
||||
|
||||
# or, specify the client id and secret separately
|
||||
app.config['GOOGLE_OAUTH2_CLIENT_ID'] = 'your-client-id'
|
||||
app.config['GOOGLE_OAUTH2_CLIENT_SECRET'] = 'your-client-secret'
|
||||
|
||||
oauth2 = UserOAuth2(app)
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Once configured, you can use the :meth:`UserOAuth2.required` decorator to
|
||||
ensure that credentials are available within a view.
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 3,7,10
|
||||
|
||||
# Note that app.route should be the outermost decorator.
|
||||
@app.route('/needs_credentials')
|
||||
@oauth2.required
|
||||
def example():
|
||||
# http is authorized with the user's credentials and can be used
|
||||
# to make http calls.
|
||||
http = oauth2.http()
|
||||
|
||||
# Or, you can access the credentials directly
|
||||
credentials = oauth2.credentials
|
||||
|
||||
If you want credentials to be optional for a view, you can leave the decorator
|
||||
off and use :meth:`UserOAuth2.has_credentials` to check.
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 3
|
||||
|
||||
@app.route('/optional')
|
||||
def optional():
|
||||
if oauth2.has_credentials():
|
||||
return 'Credentials found!'
|
||||
else:
|
||||
return 'No credentials!'
|
||||
|
||||
|
||||
When credentials are available, you can use :attr:`UserOAuth2.email` and
|
||||
:attr:`UserOAuth2.user_id` to access information from the `ID Token
|
||||
<https://developers.google.com/identity/protocols/OpenIDConnect?hl=en>`__, if
|
||||
available.
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 4
|
||||
|
||||
@app.route('/info')
|
||||
@oauth2.required
|
||||
def info():
|
||||
return "Hello, {} ({})".format(oauth2.email, oauth2.user_id)
|
||||
|
||||
|
||||
URLs & Trigging Authorization
|
||||
=============================
|
||||
|
||||
The extension will add two new routes to your application:
|
||||
|
||||
* ``"oauth2.authorize"`` -> ``/oauth2authorize``
|
||||
* ``"oauth2.callback"`` -> ``/oauth2callback``
|
||||
|
||||
When configuring your OAuth2 credentials on the Google Developer's Console, be
|
||||
sure to add ``http[s]://[your-app-url]/oauth2callback`` as an authorized
|
||||
callback url.
|
||||
|
||||
Typically you don't not need to use these routes directly, just be sure to
|
||||
decorate any views that require credentials with ``@oauth2.required``. If
|
||||
needed, you can trigger authorization at any time by redirecting the user
|
||||
to the URL returned by :meth:`UserOAuth2.authorize_url`.
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 3
|
||||
|
||||
@app.route('/login')
|
||||
def login():
|
||||
return oauth2.authorize_url("/")
|
||||
|
||||
|
||||
Incremental Auth
|
||||
================
|
||||
|
||||
This extension also supports `Incremental Auth <https://developers.google.com\
|
||||
/identity/protocols/OAuth2WebServer?hl=en#incrementalAuth>`__. To enable it,
|
||||
configure the extension with ``include_granted_scopes``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
oauth2 = UserOAuth2(app, include_granted_scopes=True)
|
||||
|
||||
Then specify any additional scopes needed on the decorator, for example:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 2,7
|
||||
|
||||
@app.route('/drive')
|
||||
@oauth2.required(scopes=["https://www.googleapis.com/auth/drive"])
|
||||
def requires_drive():
|
||||
...
|
||||
|
||||
@app.route('/calendar')
|
||||
@oauth2.required(scopes=["https://www.googleapis.com/auth/calendar"])
|
||||
def requires_calendar():
|
||||
...
|
||||
|
||||
The decorator will ensure that the the user has authorized all specified scopes
|
||||
before allowing them to access the view, and will also ensure that credentials
|
||||
do not lose any previously authorized scopes.
|
||||
|
||||
|
||||
Storage
|
||||
=======
|
||||
|
||||
By default, the extension uses a Flask session-based storage solution. This
|
||||
means that credentials are only available for the duration of a session. It
|
||||
also means that with Flask's default configuration, the credentials will be
|
||||
visible in the session cookie. It's highly recommended to use database-backed
|
||||
session and to use https whenever handling user credentials.
|
||||
|
||||
If you need the credentials to be available longer than a user session or
|
||||
available outside of a request context, you will need to implement your own
|
||||
:class:`oauth2client.Storage`.
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
|
||||
try:
|
||||
from flask import Blueprint
|
||||
from flask import _app_ctx_stack
|
||||
from flask import current_app
|
||||
from flask import redirect
|
||||
from flask import request
|
||||
from flask import session
|
||||
from flask import url_for
|
||||
except ImportError: # pragma: NO COVER
|
||||
raise ImportError('The flask utilities require flask 0.9 or newer.')
|
||||
|
||||
import httplib2
|
||||
import six.moves.http_client as httplib
|
||||
|
||||
from oauth2client import client
|
||||
from oauth2client import clientsecrets
|
||||
from oauth2client.contrib import dictionary_storage
|
||||
|
||||
|
||||
__author__ = 'jonwayne@google.com (Jon Wayne Parrott)'
|
||||
|
||||
_DEFAULT_SCOPES = ('email',)
|
||||
_CREDENTIALS_KEY = 'google_oauth2_credentials'
|
||||
_FLOW_KEY = 'google_oauth2_flow_{0}'
|
||||
_CSRF_KEY = 'google_oauth2_csrf_token'
|
||||
|
||||
|
||||
def _get_flow_for_token(csrf_token):
|
||||
"""Retrieves the flow instance associated with a given CSRF token from
|
||||
the Flask session."""
|
||||
flow_pickle = session.pop(
|
||||
_FLOW_KEY.format(csrf_token), None)
|
||||
|
||||
if flow_pickle is None:
|
||||
return None
|
||||
else:
|
||||
return pickle.loads(flow_pickle)
|
||||
|
||||
|
||||
class UserOAuth2(object):
|
||||
"""Flask extension for making OAuth 2.0 easier.
|
||||
|
||||
Configuration values:
|
||||
|
||||
* ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` path to a client secrets json
|
||||
file, obtained from the credentials screen in the Google Developers
|
||||
console.
|
||||
* ``GOOGLE_OAUTH2_CLIENT_ID`` the oauth2 credentials' client ID. This
|
||||
is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` is not
|
||||
specified.
|
||||
* ``GOOGLE_OAUTH2_CLIENT_SECRET`` the oauth2 credentials' client
|
||||
secret. This is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE``
|
||||
is not specified.
|
||||
|
||||
If app is specified, all arguments will be passed along to init_app.
|
||||
|
||||
If no app is specified, then you should call init_app in your application
|
||||
factory to finish initialization.
|
||||
"""
|
||||
|
||||
def __init__(self, app=None, *args, **kwargs):
|
||||
self.app = app
|
||||
if app is not None:
|
||||
self.init_app(app, *args, **kwargs)
|
||||
|
||||
def init_app(self, app, scopes=None, client_secrets_file=None,
|
||||
client_id=None, client_secret=None, authorize_callback=None,
|
||||
storage=None, **kwargs):
|
||||
"""Initialize this extension for the given app.
|
||||
|
||||
Arguments:
|
||||
app: A Flask application.
|
||||
scopes: Optional list of scopes to authorize.
|
||||
client_secrets_file: Path to a file containing client secrets. You
|
||||
can also specify the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE config
|
||||
value.
|
||||
client_id: If not specifying a client secrets file, specify the
|
||||
OAuth2 client id. You can also specify the
|
||||
GOOGLE_OAUTH2_CLIENT_ID config value. You must also provide a
|
||||
client secret.
|
||||
client_secret: The OAuth2 client secret. You can also specify the
|
||||
GOOGLE_OAUTH2_CLIENT_SECRET config value.
|
||||
authorize_callback: A function that is executed after successful
|
||||
user authorization.
|
||||
storage: A oauth2client.client.Storage subclass for storing the
|
||||
credentials. By default, this is a Flask session based storage.
|
||||
kwargs: Any additional args are passed along to the Flow
|
||||
constructor.
|
||||
"""
|
||||
self.app = app
|
||||
self.authorize_callback = authorize_callback
|
||||
self.flow_kwargs = kwargs
|
||||
|
||||
if storage is None:
|
||||
storage = dictionary_storage.DictionaryStorage(
|
||||
session, key=_CREDENTIALS_KEY)
|
||||
self.storage = storage
|
||||
|
||||
if scopes is None:
|
||||
scopes = app.config.get('GOOGLE_OAUTH2_SCOPES', _DEFAULT_SCOPES)
|
||||
self.scopes = scopes
|
||||
|
||||
self._load_config(client_secrets_file, client_id, client_secret)
|
||||
|
||||
app.register_blueprint(self._create_blueprint())
|
||||
|
||||
def _load_config(self, client_secrets_file, client_id, client_secret):
|
||||
"""Loads oauth2 configuration in order of priority.
|
||||
|
||||
Priority:
|
||||
1. Config passed to the constructor or init_app.
|
||||
2. Config passed via the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE app
|
||||
config.
|
||||
3. Config passed via the GOOGLE_OAUTH2_CLIENT_ID and
|
||||
GOOGLE_OAUTH2_CLIENT_SECRET app config.
|
||||
|
||||
Raises:
|
||||
ValueError if no config could be found.
|
||||
"""
|
||||
if client_id and client_secret:
|
||||
self.client_id, self.client_secret = client_id, client_secret
|
||||
return
|
||||
|
||||
if client_secrets_file:
|
||||
self._load_client_secrets(client_secrets_file)
|
||||
return
|
||||
|
||||
if 'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE' in self.app.config:
|
||||
self._load_client_secrets(
|
||||
self.app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE'])
|
||||
return
|
||||
|
||||
try:
|
||||
self.client_id, self.client_secret = (
|
||||
self.app.config['GOOGLE_OAUTH2_CLIENT_ID'],
|
||||
self.app.config['GOOGLE_OAUTH2_CLIENT_SECRET'])
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
'OAuth2 configuration could not be found. Either specify the '
|
||||
'client_secrets_file or client_id and client_secret or set '
|
||||
'the app configuration variables '
|
||||
'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE or '
|
||||
'GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET.')
|
||||
|
||||
def _load_client_secrets(self, filename):
|
||||
"""Loads client secrets from the given filename."""
|
||||
client_type, client_info = clientsecrets.loadfile(filename)
|
||||
if client_type != clientsecrets.TYPE_WEB:
|
||||
raise ValueError(
|
||||
'The flow specified in {0} is not supported.'.format(
|
||||
client_type))
|
||||
|
||||
self.client_id = client_info['client_id']
|
||||
self.client_secret = client_info['client_secret']
|
||||
|
||||
def _make_flow(self, return_url=None, **kwargs):
|
||||
"""Creates a Web Server Flow"""
|
||||
# Generate a CSRF token to prevent malicious requests.
|
||||
csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
|
||||
|
||||
session[_CSRF_KEY] = csrf_token
|
||||
|
||||
state = json.dumps({
|
||||
'csrf_token': csrf_token,
|
||||
'return_url': return_url
|
||||
})
|
||||
|
||||
kw = self.flow_kwargs.copy()
|
||||
kw.update(kwargs)
|
||||
|
||||
extra_scopes = kw.pop('scopes', [])
|
||||
scopes = set(self.scopes).union(set(extra_scopes))
|
||||
|
||||
flow = client.OAuth2WebServerFlow(
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
scope=scopes,
|
||||
state=state,
|
||||
redirect_uri=url_for('oauth2.callback', _external=True),
|
||||
**kw)
|
||||
|
||||
flow_key = _FLOW_KEY.format(csrf_token)
|
||||
session[flow_key] = pickle.dumps(flow)
|
||||
|
||||
return flow
|
||||
|
||||
def _create_blueprint(self):
|
||||
bp = Blueprint('oauth2', __name__)
|
||||
bp.add_url_rule('/oauth2authorize', 'authorize', self.authorize_view)
|
||||
bp.add_url_rule('/oauth2callback', 'callback', self.callback_view)
|
||||
|
||||
return bp
|
||||
|
||||
def authorize_view(self):
|
||||
"""Flask view that starts the authorization flow.
|
||||
|
||||
Starts flow by redirecting the user to the OAuth2 provider.
|
||||
"""
|
||||
args = request.args.to_dict()
|
||||
|
||||
# Scopes will be passed as mutliple args, and to_dict() will only
|
||||
# return one. So, we use getlist() to get all of the scopes.
|
||||
args['scopes'] = request.args.getlist('scopes')
|
||||
|
||||
return_url = args.pop('return_url', None)
|
||||
if return_url is None:
|
||||
return_url = request.referrer or '/'
|
||||
|
||||
flow = self._make_flow(return_url=return_url, **args)
|
||||
auth_url = flow.step1_get_authorize_url()
|
||||
|
||||
return redirect(auth_url)
|
||||
|
||||
def callback_view(self):
|
||||
"""Flask view that handles the user's return from OAuth2 provider.
|
||||
|
||||
On return, exchanges the authorization code for credentials and stores
|
||||
the credentials.
|
||||
"""
|
||||
if 'error' in request.args:
|
||||
reason = request.args.get(
|
||||
'error_description', request.args.get('error', ''))
|
||||
return ('Authorization failed: {0}'.format(reason),
|
||||
httplib.BAD_REQUEST)
|
||||
|
||||
try:
|
||||
encoded_state = request.args['state']
|
||||
server_csrf = session[_CSRF_KEY]
|
||||
code = request.args['code']
|
||||
except KeyError:
|
||||
return 'Invalid request', httplib.BAD_REQUEST
|
||||
|
||||
try:
|
||||
state = json.loads(encoded_state)
|
||||
client_csrf = state['csrf_token']
|
||||
return_url = state['return_url']
|
||||
except (ValueError, KeyError):
|
||||
return 'Invalid request state', httplib.BAD_REQUEST
|
||||
|
||||
if client_csrf != server_csrf:
|
||||
return 'Invalid request state', httplib.BAD_REQUEST
|
||||
|
||||
flow = _get_flow_for_token(server_csrf)
|
||||
|
||||
if flow is None:
|
||||
return 'Invalid request state', httplib.BAD_REQUEST
|
||||
|
||||
# Exchange the auth code for credentials.
|
||||
try:
|
||||
credentials = flow.step2_exchange(code)
|
||||
except client.FlowExchangeError as exchange_error:
|
||||
current_app.logger.exception(exchange_error)
|
||||
content = 'An error occurred: {0}'.format(exchange_error)
|
||||
return content, httplib.BAD_REQUEST
|
||||
|
||||
# Save the credentials to the storage.
|
||||
self.storage.put(credentials)
|
||||
|
||||
if self.authorize_callback:
|
||||
self.authorize_callback(credentials)
|
||||
|
||||
return redirect(return_url)
|
||||
|
||||
@property
|
||||
def credentials(self):
|
||||
"""The credentials for the current user or None if unavailable."""
|
||||
ctx = _app_ctx_stack.top
|
||||
|
||||
if not hasattr(ctx, _CREDENTIALS_KEY):
|
||||
ctx.google_oauth2_credentials = self.storage.get()
|
||||
|
||||
return ctx.google_oauth2_credentials
|
||||
|
||||
def has_credentials(self):
|
||||
"""Returns True if there are valid credentials for the current user."""
|
||||
if not self.credentials:
|
||||
return False
|
||||
# Is the access token expired? If so, do we have an refresh token?
|
||||
elif (self.credentials.access_token_expired and
|
||||
not self.credentials.refresh_token):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
"""Returns the user's email address or None if there are no credentials.
|
||||
|
||||
The email address is provided by the current credentials' id_token.
|
||||
This should not be used as unique identifier as the user can change
|
||||
their email. If you need a unique identifier, use user_id.
|
||||
"""
|
||||
if not self.credentials:
|
||||
return None
|
||||
try:
|
||||
return self.credentials.id_token['email']
|
||||
except KeyError:
|
||||
current_app.logger.error(
|
||||
'Invalid id_token {0}'.format(self.credentials.id_token))
|
||||
|
||||
@property
|
||||
def user_id(self):
|
||||
"""Returns the a unique identifier for the user
|
||||
|
||||
Returns None if there are no credentials.
|
||||
|
||||
The id is provided by the current credentials' id_token.
|
||||
"""
|
||||
if not self.credentials:
|
||||
return None
|
||||
try:
|
||||
return self.credentials.id_token['sub']
|
||||
except KeyError:
|
||||
current_app.logger.error(
|
||||
'Invalid id_token {0}'.format(self.credentials.id_token))
|
||||
|
||||
def authorize_url(self, return_url, **kwargs):
|
||||
"""Creates a URL that can be used to start the authorization flow.
|
||||
|
||||
When the user is directed to the URL, the authorization flow will
|
||||
begin. Once complete, the user will be redirected to the specified
|
||||
return URL.
|
||||
|
||||
Any kwargs are passed into the flow constructor.
|
||||
"""
|
||||
return url_for('oauth2.authorize', return_url=return_url, **kwargs)
|
||||
|
||||
def required(self, decorated_function=None, scopes=None,
|
||||
**decorator_kwargs):
|
||||
"""Decorator to require OAuth2 credentials for a view.
|
||||
|
||||
If credentials are not available for the current user, then they will
|
||||
be redirected to the authorization flow. Once complete, the user will
|
||||
be redirected back to the original page.
|
||||
"""
|
||||
|
||||
def curry_wrapper(wrapped_function):
|
||||
@wraps(wrapped_function)
|
||||
def required_wrapper(*args, **kwargs):
|
||||
return_url = decorator_kwargs.pop('return_url', request.url)
|
||||
|
||||
requested_scopes = set(self.scopes)
|
||||
if scopes is not None:
|
||||
requested_scopes |= set(scopes)
|
||||
if self.has_credentials():
|
||||
requested_scopes |= self.credentials.scopes
|
||||
|
||||
requested_scopes = list(requested_scopes)
|
||||
|
||||
# Does the user have credentials and does the credentials have
|
||||
# all of the needed scopes?
|
||||
if (self.has_credentials() and
|
||||
self.credentials.has_scopes(requested_scopes)):
|
||||
return wrapped_function(*args, **kwargs)
|
||||
# Otherwise, redirect to authorization
|
||||
else:
|
||||
auth_url = self.authorize_url(
|
||||
return_url,
|
||||
scopes=requested_scopes,
|
||||
**decorator_kwargs)
|
||||
|
||||
return redirect(auth_url)
|
||||
|
||||
return required_wrapper
|
||||
|
||||
if decorated_function:
|
||||
return curry_wrapper(decorated_function)
|
||||
else:
|
||||
return curry_wrapper
|
||||
|
||||
def http(self, *args, **kwargs):
|
||||
"""Returns an authorized http instance.
|
||||
|
||||
Can only be called if there are valid credentials for the user, such
|
||||
as inside of a view that is decorated with @required.
|
||||
|
||||
Args:
|
||||
*args: Positional arguments passed to httplib2.Http constructor.
|
||||
**kwargs: Positional arguments passed to httplib2.Http constructor.
|
||||
|
||||
Raises:
|
||||
ValueError if no credentials are available.
|
||||
"""
|
||||
if not self.credentials:
|
||||
raise ValueError('No credentials available.')
|
||||
return self.credentials.authorize(httplib2.Http(*args, **kwargs))
|
162
venv/Lib/site-packages/oauth2client/contrib/gce.py
Normal file
162
venv/Lib/site-packages/oauth2client/contrib/gce.py
Normal file
|
@ -0,0 +1,162 @@
|
|||
# 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.
|
||||
|
||||
"""Utilities for Google Compute Engine
|
||||
|
||||
Utilities for making it easier to use OAuth 2.0 on Google Compute Engine.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
import httplib2
|
||||
|
||||
from oauth2client import client
|
||||
from oauth2client.contrib import _metadata
|
||||
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SCOPES_WARNING = """\
|
||||
You have requested explicit scopes to be used with a GCE service account.
|
||||
Using this argument will have no effect on the actual scopes for tokens
|
||||
requested. These scopes are set at VM instance creation time and
|
||||
can't be overridden in the request.
|
||||
"""
|
||||
|
||||
|
||||
class AppAssertionCredentials(client.AssertionCredentials):
|
||||
"""Credentials object for Compute Engine Assertion Grants
|
||||
|
||||
This object will allow a Compute Engine instance to identify itself to
|
||||
Google and other OAuth 2.0 servers that can verify assertions. It can be
|
||||
used for the purpose of accessing data stored under an account assigned to
|
||||
the Compute Engine instance itself.
|
||||
|
||||
This credential does not require a flow to instantiate because it
|
||||
represents a two legged flow, and therefore has all of the required
|
||||
information to generate and refresh its own access tokens.
|
||||
|
||||
Note that :attr:`service_account_email` and :attr:`scopes`
|
||||
will both return None until the credentials have been refreshed.
|
||||
To check whether credentials have previously been refreshed use
|
||||
:attr:`invalid`.
|
||||
"""
|
||||
|
||||
def __init__(self, email=None, *args, **kwargs):
|
||||
"""Constructor for AppAssertionCredentials
|
||||
|
||||
Args:
|
||||
email: an email that specifies the service account to use.
|
||||
Only necessary if using custom service accounts
|
||||
(see https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#createdefaultserviceaccount).
|
||||
"""
|
||||
if 'scopes' in kwargs:
|
||||
warnings.warn(_SCOPES_WARNING)
|
||||
kwargs['scopes'] = None
|
||||
|
||||
# Assertion type is no longer used, but still in the
|
||||
# parent class signature.
|
||||
super(AppAssertionCredentials, self).__init__(None, *args, **kwargs)
|
||||
|
||||
self.service_account_email = email
|
||||
self.scopes = None
|
||||
self.invalid = True
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
raise NotImplementedError(
|
||||
'Cannot serialize credentials for GCE service accounts.')
|
||||
|
||||
def to_json(self):
|
||||
raise NotImplementedError(
|
||||
'Cannot serialize credentials for GCE service accounts.')
|
||||
|
||||
def retrieve_scopes(self, http):
|
||||
"""Retrieves the canonical list of scopes for this access token.
|
||||
|
||||
Overrides client.Credentials.retrieve_scopes. Fetches scopes info
|
||||
from the metadata server.
|
||||
|
||||
Args:
|
||||
http: httplib2.Http, an http object to be used to make the refresh
|
||||
request.
|
||||
|
||||
Returns:
|
||||
A set of strings containing the canonical list of scopes.
|
||||
"""
|
||||
self._retrieve_info(http.request)
|
||||
return self.scopes
|
||||
|
||||
def _retrieve_info(self, http_request):
|
||||
"""Validates invalid service accounts by retrieving service account info.
|
||||
|
||||
Args:
|
||||
http_request: callable, a callable that matches the method
|
||||
signature of httplib2.Http.request, used to make the
|
||||
request to the metadata server
|
||||
"""
|
||||
if self.invalid:
|
||||
info = _metadata.get_service_account_info(
|
||||
http_request,
|
||||
service_account=self.service_account_email or 'default')
|
||||
self.invalid = False
|
||||
self.service_account_email = info['email']
|
||||
self.scopes = info['scopes']
|
||||
|
||||
def _refresh(self, http_request):
|
||||
"""Refreshes the access_token.
|
||||
|
||||
Skip all the storage hoops and just refresh using the API.
|
||||
|
||||
Args:
|
||||
http_request: callable, a callable that matches the method
|
||||
signature of httplib2.Http.request, used to make
|
||||
the refresh request.
|
||||
|
||||
Raises:
|
||||
HttpAccessTokenRefreshError: When the refresh fails.
|
||||
"""
|
||||
try:
|
||||
self._retrieve_info(http_request)
|
||||
self.access_token, self.token_expiry = _metadata.get_token(
|
||||
http_request, service_account=self.service_account_email)
|
||||
except httplib2.HttpLib2Error as e:
|
||||
raise client.HttpAccessTokenRefreshError(str(e))
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
raise NotImplementedError(
|
||||
'Cannot serialize credentials for GCE service accounts.')
|
||||
|
||||
def create_scoped_required(self):
|
||||
return False
|
||||
|
||||
def sign_blob(self, blob):
|
||||
"""Cryptographically sign a blob (of bytes).
|
||||
|
||||
This method is provided to support a common interface, but
|
||||
the actual key used for a Google Compute Engine service account
|
||||
is not available, so it can't be used to sign content.
|
||||
|
||||
Args:
|
||||
blob: bytes, Message to be signed.
|
||||
|
||||
Raises:
|
||||
NotImplementedError, always.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
'Compute Engine service accounts cannot sign blobs')
|
|
@ -0,0 +1,98 @@
|
|||
# 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.
|
||||
|
||||
"""A keyring based Storage.
|
||||
|
||||
A Storage for Credentials that uses the keyring module.
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
import keyring
|
||||
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
|
||||
class Storage(client.Storage):
|
||||
"""Store and retrieve a single credential to and from the keyring.
|
||||
|
||||
To use this module you must have the keyring module installed. See
|
||||
<http://pypi.python.org/pypi/keyring/>. This is an optional module and is
|
||||
not installed with oauth2client by default because it does not work on all
|
||||
the platforms that oauth2client supports, such as Google App Engine.
|
||||
|
||||
The keyring module <http://pypi.python.org/pypi/keyring/> is a
|
||||
cross-platform library for access the keyring capabilities of the local
|
||||
system. The user will be prompted for their keyring password when this
|
||||
module is used, and the manner in which the user is prompted will vary per
|
||||
platform.
|
||||
|
||||
Usage::
|
||||
|
||||
from oauth2client import keyring_storage
|
||||
|
||||
s = keyring_storage.Storage('name_of_application', 'user1')
|
||||
credentials = s.get()
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, service_name, user_name):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
service_name: string, The name of the service under which the
|
||||
credentials are stored.
|
||||
user_name: string, The name of the user to store credentials for.
|
||||
"""
|
||||
super(Storage, self).__init__(lock=threading.Lock())
|
||||
self._service_name = service_name
|
||||
self._user_name = user_name
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve Credential from file.
|
||||
|
||||
Returns:
|
||||
oauth2client.client.Credentials
|
||||
"""
|
||||
credentials = None
|
||||
content = keyring.get_password(self._service_name, self._user_name)
|
||||
|
||||
if content is not None:
|
||||
try:
|
||||
credentials = client.Credentials.new_from_json(content)
|
||||
credentials.set_store(self)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return credentials
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Write Credentials to file.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
keyring.set_password(self._service_name, self._user_name,
|
||||
credentials.to_json())
|
||||
|
||||
def locked_delete(self):
|
||||
"""Delete Credentials file.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
keyring.set_password(self._service_name, self._user_name, '')
|
234
venv/Lib/site-packages/oauth2client/contrib/locked_file.py
Normal file
234
venv/Lib/site-packages/oauth2client/contrib/locked_file.py
Normal file
|
@ -0,0 +1,234 @@
|
|||
# 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()
|
|
@ -0,0 +1,355 @@
|
|||
# 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.
|
||||
|
||||
"""Multiprocess file credential storage.
|
||||
|
||||
This module provides file-based storage that supports multiple credentials and
|
||||
cross-thread and process access.
|
||||
|
||||
This module supersedes the functionality previously found in `multistore_file`.
|
||||
|
||||
This module provides :class:`MultiprocessFileStorage` which:
|
||||
* Is tied to a single credential via a user-specified key. This key can be
|
||||
used to distinguish between multiple users, client ids, and/or scopes.
|
||||
* Can be safely accessed and refreshed across threads and processes.
|
||||
|
||||
Process & thread safety guarantees the following behavior:
|
||||
* If one thread or process refreshes a credential, subsequent refreshes
|
||||
from other processes will re-fetch the credentials from the file instead
|
||||
of performing an http request.
|
||||
* If two processes or threads attempt to refresh concurrently, only one
|
||||
will be able to acquire the lock and refresh, with the deadlock caveat
|
||||
below.
|
||||
* The interprocess lock will not deadlock, instead, the if a process can
|
||||
not acquire the interprocess lock within ``INTERPROCESS_LOCK_DEADLINE``
|
||||
it will allow refreshing the credential but will not write the updated
|
||||
credential to disk, This logic happens during every lock cycle - if the
|
||||
credentials are refreshed again it will retry locking and writing as
|
||||
normal.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Before using the storage, you need to decide how you want to key the
|
||||
credentials. A few common strategies include:
|
||||
|
||||
* If you're storing credentials for multiple users in a single file, use
|
||||
a unique identifier for each user as the key.
|
||||
* If you're storing credentials for multiple client IDs in a single file,
|
||||
use the client ID as the key.
|
||||
* If you're storing multiple credentials for one user, use the scopes as
|
||||
the key.
|
||||
* If you have a complicated setup, use a compound key. For example, you
|
||||
can use a combination of the client ID and scopes as the key.
|
||||
|
||||
Create an instance of :class:`MultiprocessFileStorage` for each credential you
|
||||
want to store, for example::
|
||||
|
||||
filename = 'credentials'
|
||||
key = '{}-{}'.format(client_id, user_id)
|
||||
storage = MultiprocessFileStorage(filename, key)
|
||||
|
||||
To store the credentials::
|
||||
|
||||
storage.put(credentials)
|
||||
|
||||
If you're going to continue to use the credentials after storing them, be sure
|
||||
to call :func:`set_store`::
|
||||
|
||||
credentials.set_store(storage)
|
||||
|
||||
To retrieve the credentials::
|
||||
|
||||
storage.get(credentials)
|
||||
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
import fasteners
|
||||
from six import iteritems
|
||||
|
||||
from oauth2client import _helpers
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
#: The maximum amount of time, in seconds, to wait when acquire the
|
||||
#: interprocess lock before falling back to read-only mode.
|
||||
INTERPROCESS_LOCK_DEADLINE = 1
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_backends = {}
|
||||
_backends_lock = threading.Lock()
|
||||
|
||||
|
||||
def _create_file_if_needed(filename):
|
||||
"""Creates the an empty file if it does not already exist.
|
||||
|
||||
Returns:
|
||||
True if the file was created, False otherwise.
|
||||
"""
|
||||
if os.path.exists(filename):
|
||||
return False
|
||||
else:
|
||||
# Equivalent to "touch".
|
||||
open(filename, 'a+b').close()
|
||||
logger.info('Credential file {0} created'.format(filename))
|
||||
return True
|
||||
|
||||
|
||||
def _load_credentials_file(credentials_file):
|
||||
"""Load credentials from the given file handle.
|
||||
|
||||
The file is expected to be in this format:
|
||||
|
||||
{
|
||||
"file_version": 2,
|
||||
"credentials": {
|
||||
"key": "base64 encoded json representation of credentials."
|
||||
}
|
||||
}
|
||||
|
||||
This function will warn and return empty credentials instead of raising
|
||||
exceptions.
|
||||
|
||||
Args:
|
||||
credentials_file: An open file handle.
|
||||
|
||||
Returns:
|
||||
A dictionary mapping user-defined keys to an instance of
|
||||
:class:`oauth2client.client.Credentials`.
|
||||
"""
|
||||
try:
|
||||
credentials_file.seek(0)
|
||||
data = json.load(credentials_file)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
'Credentials file could not be loaded, will ignore and '
|
||||
'overwrite.')
|
||||
return {}
|
||||
|
||||
if data.get('file_version') != 2:
|
||||
logger.warning(
|
||||
'Credentials file is not version 2, will ignore and '
|
||||
'overwrite.')
|
||||
return {}
|
||||
|
||||
credentials = {}
|
||||
|
||||
for key, encoded_credential in iteritems(data.get('credentials', {})):
|
||||
try:
|
||||
credential_json = base64.b64decode(encoded_credential)
|
||||
credential = client.Credentials.new_from_json(credential_json)
|
||||
credentials[key] = credential
|
||||
except:
|
||||
logger.warning(
|
||||
'Invalid credential {0} in file, ignoring.'.format(key))
|
||||
|
||||
return credentials
|
||||
|
||||
|
||||
def _write_credentials_file(credentials_file, credentials):
|
||||
"""Writes credentials to a file.
|
||||
|
||||
Refer to :func:`_load_credentials_file` for the format.
|
||||
|
||||
Args:
|
||||
credentials_file: An open file handle, must be read/write.
|
||||
credentials: A dictionary mapping user-defined keys to an instance of
|
||||
:class:`oauth2client.client.Credentials`.
|
||||
"""
|
||||
data = {'file_version': 2, 'credentials': {}}
|
||||
|
||||
for key, credential in iteritems(credentials):
|
||||
credential_json = credential.to_json()
|
||||
encoded_credential = _helpers._from_bytes(base64.b64encode(
|
||||
_helpers._to_bytes(credential_json)))
|
||||
data['credentials'][key] = encoded_credential
|
||||
|
||||
credentials_file.seek(0)
|
||||
json.dump(data, credentials_file)
|
||||
credentials_file.truncate()
|
||||
|
||||
|
||||
class _MultiprocessStorageBackend(object):
|
||||
"""Thread-local backend for multiprocess storage.
|
||||
|
||||
Each process has only one instance of this backend per file. All threads
|
||||
share a single instance of this backend. This ensures that all threads
|
||||
use the same thread lock and process lock when accessing the file.
|
||||
"""
|
||||
|
||||
def __init__(self, filename):
|
||||
self._file = None
|
||||
self._filename = filename
|
||||
self._process_lock = fasteners.InterProcessLock(
|
||||
'{0}.lock'.format(filename))
|
||||
self._thread_lock = threading.Lock()
|
||||
self._read_only = False
|
||||
self._credentials = {}
|
||||
|
||||
def _load_credentials(self):
|
||||
"""(Re-)loads the credentials from the file."""
|
||||
if not self._file:
|
||||
return
|
||||
|
||||
loaded_credentials = _load_credentials_file(self._file)
|
||||
self._credentials.update(loaded_credentials)
|
||||
|
||||
logger.debug('Read credential file')
|
||||
|
||||
def _write_credentials(self):
|
||||
if self._read_only:
|
||||
logger.debug('In read-only mode, not writing credentials.')
|
||||
return
|
||||
|
||||
_write_credentials_file(self._file, self._credentials)
|
||||
logger.debug('Wrote credential file {0}.'.format(self._filename))
|
||||
|
||||
def acquire_lock(self):
|
||||
self._thread_lock.acquire()
|
||||
locked = self._process_lock.acquire(timeout=INTERPROCESS_LOCK_DEADLINE)
|
||||
|
||||
if locked:
|
||||
_create_file_if_needed(self._filename)
|
||||
self._file = open(self._filename, 'r+')
|
||||
self._read_only = False
|
||||
|
||||
else:
|
||||
logger.warn(
|
||||
'Failed to obtain interprocess lock for credentials. '
|
||||
'If a credential is being refreshed, other processes may '
|
||||
'not see the updated access token and refresh as well.')
|
||||
if os.path.exists(self._filename):
|
||||
self._file = open(self._filename, 'r')
|
||||
else:
|
||||
self._file = None
|
||||
self._read_only = True
|
||||
|
||||
self._load_credentials()
|
||||
|
||||
def release_lock(self):
|
||||
if self._file is not None:
|
||||
self._file.close()
|
||||
self._file = None
|
||||
|
||||
if not self._read_only:
|
||||
self._process_lock.release()
|
||||
|
||||
self._thread_lock.release()
|
||||
|
||||
def _refresh_predicate(self, credentials):
|
||||
if credentials is None:
|
||||
return True
|
||||
elif credentials.invalid:
|
||||
return True
|
||||
elif credentials.access_token_expired:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def locked_get(self, key):
|
||||
# Check if the credential is already in memory.
|
||||
credentials = self._credentials.get(key, None)
|
||||
|
||||
# Use the refresh predicate to determine if the entire store should be
|
||||
# reloaded. This basically checks if the credentials are invalid
|
||||
# or expired. This covers the situation where another process has
|
||||
# refreshed the credentials and this process doesn't know about it yet.
|
||||
# In that case, this process won't needlessly refresh the credentials.
|
||||
if self._refresh_predicate(credentials):
|
||||
self._load_credentials()
|
||||
credentials = self._credentials.get(key, None)
|
||||
|
||||
return credentials
|
||||
|
||||
def locked_put(self, key, credentials):
|
||||
self._load_credentials()
|
||||
self._credentials[key] = credentials
|
||||
self._write_credentials()
|
||||
|
||||
def locked_delete(self, key):
|
||||
self._load_credentials()
|
||||
self._credentials.pop(key, None)
|
||||
self._write_credentials()
|
||||
|
||||
|
||||
def _get_backend(filename):
|
||||
"""A helper method to get or create a backend with thread locking.
|
||||
|
||||
This ensures that only one backend is used per-file per-process, so that
|
||||
thread and process locks are appropriately shared.
|
||||
|
||||
Args:
|
||||
filename: The full path to the credential storage file.
|
||||
|
||||
Returns:
|
||||
An instance of :class:`_MultiprocessStorageBackend`.
|
||||
"""
|
||||
filename = os.path.abspath(filename)
|
||||
|
||||
with _backends_lock:
|
||||
if filename not in _backends:
|
||||
_backends[filename] = _MultiprocessStorageBackend(filename)
|
||||
return _backends[filename]
|
||||
|
||||
|
||||
class MultiprocessFileStorage(client.Storage):
|
||||
"""Multiprocess file credential storage.
|
||||
|
||||
Args:
|
||||
filename: The path to the file where credentials will be stored.
|
||||
key: An arbitrary string used to uniquely identify this set of
|
||||
credentials. For example, you may use the user's ID as the key or
|
||||
a combination of the client ID and user ID.
|
||||
"""
|
||||
def __init__(self, filename, key):
|
||||
self._key = key
|
||||
self._backend = _get_backend(filename)
|
||||
|
||||
def acquire_lock(self):
|
||||
self._backend.acquire_lock()
|
||||
|
||||
def release_lock(self):
|
||||
self._backend.release_lock()
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieves the current credentials from the store.
|
||||
|
||||
Returns:
|
||||
An instance of :class:`oauth2client.client.Credentials` or `None`.
|
||||
"""
|
||||
credential = self._backend.locked_get(self._key)
|
||||
|
||||
if credential is not None:
|
||||
credential.set_store(self)
|
||||
|
||||
return credential
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Writes the given credentials to the store.
|
||||
|
||||
Args:
|
||||
credentials: an instance of
|
||||
:class:`oauth2client.client.Credentials`.
|
||||
"""
|
||||
return self._backend.locked_put(self._key, credentials)
|
||||
|
||||
def locked_delete(self):
|
||||
"""Deletes the current credentials from the store."""
|
||||
return self._backend.locked_delete(self._key)
|
505
venv/Lib/site-packages/oauth2client/contrib/multistore_file.py
Normal file
505
venv/Lib/site-packages/oauth2client/contrib/multistore_file.py
Normal file
|
@ -0,0 +1,505 @@
|
|||
# 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)
|
173
venv/Lib/site-packages/oauth2client/contrib/sqlalchemy.py
Normal file
173
venv/Lib/site-packages/oauth2client/contrib/sqlalchemy.py
Normal file
|
@ -0,0 +1,173 @@
|
|||
# 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.
|
||||
|
||||
"""OAuth 2.0 utilities for SQLAlchemy.
|
||||
|
||||
Utilities for using OAuth 2.0 in conjunction with a SQLAlchemy.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
In order to use this storage, you'll need to create table
|
||||
with :class:`oauth2client.contrib.sqlalchemy.CredentialsType` column.
|
||||
It's recommended to either put this column on some sort of user info
|
||||
table or put the column in a table with a belongs-to relationship to
|
||||
a user info table.
|
||||
|
||||
Here's an example of a simple table with a :class:`CredentialsType`
|
||||
column that's related to a user table by the `user_id` key.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Integer
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from oauth2client.contrib.sqlalchemy import CredentialsType
|
||||
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class Credentials(Base):
|
||||
__tablename__ = 'credentials'
|
||||
|
||||
user_id = Column(Integer, ForeignKey('user.id'))
|
||||
credentials = Column(CredentialsType)
|
||||
|
||||
|
||||
class User(Base):
|
||||
id = Column(Integer, primary_key=True)
|
||||
# bunch of other columns
|
||||
credentials = relationship('Credentials')
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
With tables ready, you are now able to store credentials in database.
|
||||
We will reuse tables defined above.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from oauth2client.client import OAuth2Credentials
|
||||
from oauth2client.contrib.sql_alchemy import Storage
|
||||
|
||||
session = Session()
|
||||
user = session.query(User).first()
|
||||
storage = Storage(
|
||||
session=session,
|
||||
model_class=Credentials,
|
||||
# This is the key column used to identify
|
||||
# the row that stores the credentials.
|
||||
key_name='user_id',
|
||||
key_value=user.id,
|
||||
property_name='credentials',
|
||||
)
|
||||
|
||||
# Store
|
||||
credentials = OAuth2Credentials(...)
|
||||
storage.put(credentials)
|
||||
|
||||
# Retrieve
|
||||
credentials = storage.get()
|
||||
|
||||
# Delete
|
||||
storage.delete()
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sqlalchemy.types
|
||||
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
class CredentialsType(sqlalchemy.types.PickleType):
|
||||
"""Type representing credentials.
|
||||
|
||||
Alias for :class:`sqlalchemy.types.PickleType`.
|
||||
"""
|
||||
|
||||
|
||||
class Storage(client.Storage):
|
||||
"""Store and retrieve a single credential to and from SQLAlchemy.
|
||||
This helper presumes the Credentials
|
||||
have been stored as a Credentials column
|
||||
on a db model class.
|
||||
"""
|
||||
|
||||
def __init__(self, session, model_class, key_name,
|
||||
key_value, property_name):
|
||||
"""Constructor for Storage.
|
||||
|
||||
Args:
|
||||
session: An instance of :class:`sqlalchemy.orm.Session`.
|
||||
model_class: SQLAlchemy declarative mapping.
|
||||
key_name: string, key name for the entity that has the credentials
|
||||
key_value: key value for the entity that has the credentials
|
||||
property_name: A string indicating which property on the
|
||||
``model_class`` to store the credentials.
|
||||
This property must be a
|
||||
:class:`CredentialsType` column.
|
||||
"""
|
||||
super(Storage, self).__init__()
|
||||
|
||||
self.session = session
|
||||
self.model_class = model_class
|
||||
self.key_name = key_name
|
||||
self.key_value = key_value
|
||||
self.property_name = property_name
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve stored credential.
|
||||
|
||||
Returns:
|
||||
A :class:`oauth2client.Credentials` instance or `None`.
|
||||
"""
|
||||
filters = {self.key_name: self.key_value}
|
||||
query = self.session.query(self.model_class).filter_by(**filters)
|
||||
entity = query.first()
|
||||
|
||||
if entity:
|
||||
credential = getattr(entity, self.property_name)
|
||||
if credential and hasattr(credential, 'set_store'):
|
||||
credential.set_store(self)
|
||||
return credential
|
||||
else:
|
||||
return None
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Write a credentials to the SQLAlchemy datastore.
|
||||
|
||||
Args:
|
||||
credentials: :class:`oauth2client.Credentials`
|
||||
"""
|
||||
filters = {self.key_name: self.key_value}
|
||||
query = self.session.query(self.model_class).filter_by(**filters)
|
||||
entity = query.first()
|
||||
|
||||
if not entity:
|
||||
entity = self.model_class(**filters)
|
||||
|
||||
setattr(entity, self.property_name, credentials)
|
||||
self.session.add(entity)
|
||||
|
||||
def locked_delete(self):
|
||||
"""Delete credentials from the SQLAlchemy datastore."""
|
||||
filters = {self.key_name: self.key_value}
|
||||
self.session.query(self.model_class).filter_by(**filters).delete()
|
106
venv/Lib/site-packages/oauth2client/contrib/xsrfutil.py
Normal file
106
venv/Lib/site-packages/oauth2client/contrib/xsrfutil.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
# Copyright 2014 the Melange authors.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Helper methods for creating & verifying XSRF tokens."""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import hmac
|
||||
import time
|
||||
|
||||
from oauth2client import _helpers
|
||||
from oauth2client import util
|
||||
|
||||
__authors__ = [
|
||||
'"Doug Coker" <dcoker@google.com>',
|
||||
'"Joe Gregorio" <jcgregorio@google.com>',
|
||||
]
|
||||
|
||||
# Delimiter character
|
||||
DELIMITER = b':'
|
||||
|
||||
# 1 hour in seconds
|
||||
DEFAULT_TIMEOUT_SECS = 60 * 60
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def generate_token(key, user_id, action_id='', when=None):
|
||||
"""Generates a URL-safe token for the given user, action, time tuple.
|
||||
|
||||
Args:
|
||||
key: secret key to use.
|
||||
user_id: the user ID of the authenticated user.
|
||||
action_id: a string identifier of the action they requested
|
||||
authorization for.
|
||||
when: the time in seconds since the epoch at which the user was
|
||||
authorized for this action. If not set the current time is used.
|
||||
|
||||
Returns:
|
||||
A string XSRF protection token.
|
||||
"""
|
||||
digester = hmac.new(_helpers._to_bytes(key, encoding='utf-8'))
|
||||
digester.update(_helpers._to_bytes(str(user_id), encoding='utf-8'))
|
||||
digester.update(DELIMITER)
|
||||
digester.update(_helpers._to_bytes(action_id, encoding='utf-8'))
|
||||
digester.update(DELIMITER)
|
||||
when = _helpers._to_bytes(str(when or int(time.time())), encoding='utf-8')
|
||||
digester.update(when)
|
||||
digest = digester.digest()
|
||||
|
||||
token = base64.urlsafe_b64encode(digest + DELIMITER + when)
|
||||
return token
|
||||
|
||||
|
||||
@util.positional(3)
|
||||
def validate_token(key, token, user_id, action_id="", current_time=None):
|
||||
"""Validates that the given token authorizes the user for the action.
|
||||
|
||||
Tokens are invalid if the time of issue is too old or if the token
|
||||
does not match what generateToken outputs (i.e. the token was forged).
|
||||
|
||||
Args:
|
||||
key: secret key to use.
|
||||
token: a string of the token generated by generateToken.
|
||||
user_id: the user ID of the authenticated user.
|
||||
action_id: a string identifier of the action they requested
|
||||
authorization for.
|
||||
|
||||
Returns:
|
||||
A boolean - True if the user is authorized for the action, False
|
||||
otherwise.
|
||||
"""
|
||||
if not token:
|
||||
return False
|
||||
try:
|
||||
decoded = base64.urlsafe_b64decode(token)
|
||||
token_time = int(decoded.split(DELIMITER)[-1])
|
||||
except (TypeError, ValueError, binascii.Error):
|
||||
return False
|
||||
if current_time is None:
|
||||
current_time = time.time()
|
||||
# If the token is too old it's not valid.
|
||||
if current_time - token_time > DEFAULT_TIMEOUT_SECS:
|
||||
return False
|
||||
|
||||
# The given token should match the generated one with the same time.
|
||||
expected_token = generate_token(key, user_id, action_id=action_id,
|
||||
when=token_time)
|
||||
if len(token) != len(expected_token):
|
||||
return False
|
||||
|
||||
# Perform constant time comparison to avoid timing attacks
|
||||
different = 0
|
||||
for x, y in zip(bytearray(token), bytearray(expected_token)):
|
||||
different |= x ^ y
|
||||
return not different
|
250
venv/Lib/site-packages/oauth2client/crypt.py
Normal file
250
venv/Lib/site-packages/oauth2client/crypt.py
Normal file
|
@ -0,0 +1,250 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# 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.
|
||||
"""Crypto-related routines for oauth2client."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
from oauth2client import _helpers
|
||||
from oauth2client import _pure_python_crypt
|
||||
|
||||
|
||||
RsaSigner = _pure_python_crypt.RsaSigner
|
||||
RsaVerifier = _pure_python_crypt.RsaVerifier
|
||||
|
||||
CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
|
||||
AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds
|
||||
MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AppIdentityError(Exception):
|
||||
"""Error to indicate crypto failure."""
|
||||
|
||||
|
||||
def _bad_pkcs12_key_as_pem(*args, **kwargs):
|
||||
raise NotImplementedError('pkcs12_key_as_pem requires OpenSSL.')
|
||||
|
||||
|
||||
try:
|
||||
from oauth2client import _openssl_crypt
|
||||
OpenSSLSigner = _openssl_crypt.OpenSSLSigner
|
||||
OpenSSLVerifier = _openssl_crypt.OpenSSLVerifier
|
||||
pkcs12_key_as_pem = _openssl_crypt.pkcs12_key_as_pem
|
||||
except ImportError: # pragma: NO COVER
|
||||
OpenSSLVerifier = None
|
||||
OpenSSLSigner = None
|
||||
pkcs12_key_as_pem = _bad_pkcs12_key_as_pem
|
||||
|
||||
try:
|
||||
from oauth2client import _pycrypto_crypt
|
||||
PyCryptoSigner = _pycrypto_crypt.PyCryptoSigner
|
||||
PyCryptoVerifier = _pycrypto_crypt.PyCryptoVerifier
|
||||
except ImportError: # pragma: NO COVER
|
||||
PyCryptoVerifier = None
|
||||
PyCryptoSigner = None
|
||||
|
||||
|
||||
if OpenSSLSigner:
|
||||
Signer = OpenSSLSigner
|
||||
Verifier = OpenSSLVerifier
|
||||
elif PyCryptoSigner: # pragma: NO COVER
|
||||
Signer = PyCryptoSigner
|
||||
Verifier = PyCryptoVerifier
|
||||
else: # pragma: NO COVER
|
||||
Signer = RsaSigner
|
||||
Verifier = RsaVerifier
|
||||
|
||||
|
||||
def make_signed_jwt(signer, payload, key_id=None):
|
||||
"""Make a signed JWT.
|
||||
|
||||
See http://self-issued.info/docs/draft-jones-json-web-token.html.
|
||||
|
||||
Args:
|
||||
signer: crypt.Signer, Cryptographic signer.
|
||||
payload: dict, Dictionary of data to convert to JSON and then sign.
|
||||
key_id: string, (Optional) Key ID header.
|
||||
|
||||
Returns:
|
||||
string, The JWT for the payload.
|
||||
"""
|
||||
header = {'typ': 'JWT', 'alg': 'RS256'}
|
||||
if key_id is not None:
|
||||
header['kid'] = key_id
|
||||
|
||||
segments = [
|
||||
_helpers._urlsafe_b64encode(_helpers._json_encode(header)),
|
||||
_helpers._urlsafe_b64encode(_helpers._json_encode(payload)),
|
||||
]
|
||||
signing_input = b'.'.join(segments)
|
||||
|
||||
signature = signer.sign(signing_input)
|
||||
segments.append(_helpers._urlsafe_b64encode(signature))
|
||||
|
||||
logger.debug(str(segments))
|
||||
|
||||
return b'.'.join(segments)
|
||||
|
||||
|
||||
def _verify_signature(message, signature, certs):
|
||||
"""Verifies signed content using a list of certificates.
|
||||
|
||||
Args:
|
||||
message: string or bytes, The message to verify.
|
||||
signature: string or bytes, The signature on the message.
|
||||
certs: iterable, certificates in PEM format.
|
||||
|
||||
Raises:
|
||||
AppIdentityError: If none of the certificates can verify the message
|
||||
against the signature.
|
||||
"""
|
||||
for pem in certs:
|
||||
verifier = Verifier.from_string(pem, is_x509_cert=True)
|
||||
if verifier.verify(message, signature):
|
||||
return
|
||||
|
||||
# If we have not returned, no certificate confirms the signature.
|
||||
raise AppIdentityError('Invalid token signature')
|
||||
|
||||
|
||||
def _check_audience(payload_dict, audience):
|
||||
"""Checks audience field from a JWT payload.
|
||||
|
||||
Does nothing if the passed in ``audience`` is null.
|
||||
|
||||
Args:
|
||||
payload_dict: dict, A dictionary containing a JWT payload.
|
||||
audience: string or NoneType, an audience to check for in
|
||||
the JWT payload.
|
||||
|
||||
Raises:
|
||||
AppIdentityError: If there is no ``'aud'`` field in the payload
|
||||
dictionary but there is an ``audience`` to check.
|
||||
AppIdentityError: If the ``'aud'`` field in the payload dictionary
|
||||
does not match the ``audience``.
|
||||
"""
|
||||
if audience is None:
|
||||
return
|
||||
|
||||
audience_in_payload = payload_dict.get('aud')
|
||||
if audience_in_payload is None:
|
||||
raise AppIdentityError(
|
||||
'No aud field in token: {0}'.format(payload_dict))
|
||||
if audience_in_payload != audience:
|
||||
raise AppIdentityError('Wrong recipient, {0} != {1}: {2}'.format(
|
||||
audience_in_payload, audience, payload_dict))
|
||||
|
||||
|
||||
def _verify_time_range(payload_dict):
|
||||
"""Verifies the issued at and expiration from a JWT payload.
|
||||
|
||||
Makes sure the current time (in UTC) falls between the issued at and
|
||||
expiration for the JWT (with some skew allowed for via
|
||||
``CLOCK_SKEW_SECS``).
|
||||
|
||||
Args:
|
||||
payload_dict: dict, A dictionary containing a JWT payload.
|
||||
|
||||
Raises:
|
||||
AppIdentityError: If there is no ``'iat'`` field in the payload
|
||||
dictionary.
|
||||
AppIdentityError: If there is no ``'exp'`` field in the payload
|
||||
dictionary.
|
||||
AppIdentityError: If the JWT expiration is too far in the future (i.e.
|
||||
if the expiration would imply a token lifetime
|
||||
longer than what is allowed.)
|
||||
AppIdentityError: If the token appears to have been issued in the
|
||||
future (up to clock skew).
|
||||
AppIdentityError: If the token appears to have expired in the past
|
||||
(up to clock skew).
|
||||
"""
|
||||
# Get the current time to use throughout.
|
||||
now = int(time.time())
|
||||
|
||||
# Make sure issued at and expiration are in the payload.
|
||||
issued_at = payload_dict.get('iat')
|
||||
if issued_at is None:
|
||||
raise AppIdentityError(
|
||||
'No iat field in token: {0}'.format(payload_dict))
|
||||
expiration = payload_dict.get('exp')
|
||||
if expiration is None:
|
||||
raise AppIdentityError(
|
||||
'No exp field in token: {0}'.format(payload_dict))
|
||||
|
||||
# Make sure the expiration gives an acceptable token lifetime.
|
||||
if expiration >= now + MAX_TOKEN_LIFETIME_SECS:
|
||||
raise AppIdentityError(
|
||||
'exp field too far in future: {0}'.format(payload_dict))
|
||||
|
||||
# Make sure (up to clock skew) that the token wasn't issued in the future.
|
||||
earliest = issued_at - CLOCK_SKEW_SECS
|
||||
if now < earliest:
|
||||
raise AppIdentityError('Token used too early, {0} < {1}: {2}'.format(
|
||||
now, earliest, payload_dict))
|
||||
# Make sure (up to clock skew) that the token isn't already expired.
|
||||
latest = expiration + CLOCK_SKEW_SECS
|
||||
if now > latest:
|
||||
raise AppIdentityError('Token used too late, {0} > {1}: {2}'.format(
|
||||
now, latest, payload_dict))
|
||||
|
||||
|
||||
def verify_signed_jwt_with_certs(jwt, certs, audience=None):
|
||||
"""Verify a JWT against public certs.
|
||||
|
||||
See http://self-issued.info/docs/draft-jones-json-web-token.html.
|
||||
|
||||
Args:
|
||||
jwt: string, A JWT.
|
||||
certs: dict, Dictionary where values of public keys in PEM format.
|
||||
audience: string, The audience, 'aud', that this JWT should contain. If
|
||||
None then the JWT's 'aud' parameter is not verified.
|
||||
|
||||
Returns:
|
||||
dict, The deserialized JSON payload in the JWT.
|
||||
|
||||
Raises:
|
||||
AppIdentityError: if any checks are failed.
|
||||
"""
|
||||
jwt = _helpers._to_bytes(jwt)
|
||||
|
||||
if jwt.count(b'.') != 2:
|
||||
raise AppIdentityError(
|
||||
'Wrong number of segments in token: {0}'.format(jwt))
|
||||
|
||||
header, payload, signature = jwt.split(b'.')
|
||||
message_to_sign = header + b'.' + payload
|
||||
signature = _helpers._urlsafe_b64decode(signature)
|
||||
|
||||
# Parse token.
|
||||
payload_bytes = _helpers._urlsafe_b64decode(payload)
|
||||
try:
|
||||
payload_dict = json.loads(_helpers._from_bytes(payload_bytes))
|
||||
except:
|
||||
raise AppIdentityError('Can\'t parse token: {0}'.format(payload_bytes))
|
||||
|
||||
# Verify that the signature matches the message.
|
||||
_verify_signature(message_to_sign, signature, certs.values())
|
||||
|
||||
# Verify the issued at and created times in the payload.
|
||||
_verify_time_range(payload_dict)
|
||||
|
||||
# Check audience.
|
||||
_check_audience(payload_dict, audience)
|
||||
|
||||
return payload_dict
|
106
venv/Lib/site-packages/oauth2client/file.py
Normal file
106
venv/Lib/site-packages/oauth2client/file.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
# 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.
|
||||
|
||||
"""Utilities for OAuth.
|
||||
|
||||
Utilities for making it easier to work with OAuth 2.0
|
||||
credentials.
|
||||
"""
|
||||
|
||||
import os
|
||||
import threading
|
||||
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
|
||||
class CredentialsFileSymbolicLinkError(Exception):
|
||||
"""Credentials files must not be symbolic links."""
|
||||
|
||||
|
||||
class Storage(client.Storage):
|
||||
"""Store and retrieve a single credential to and from a file."""
|
||||
|
||||
def __init__(self, filename):
|
||||
super(Storage, self).__init__(lock=threading.Lock())
|
||||
self._filename = filename
|
||||
|
||||
def _validate_file(self):
|
||||
if os.path.islink(self._filename):
|
||||
raise CredentialsFileSymbolicLinkError(
|
||||
'File: {0} is a symbolic link.'.format(self._filename))
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve Credential from file.
|
||||
|
||||
Returns:
|
||||
oauth2client.client.Credentials
|
||||
|
||||
Raises:
|
||||
CredentialsFileSymbolicLinkError if the file is a symbolic link.
|
||||
"""
|
||||
credentials = None
|
||||
self._validate_file()
|
||||
try:
|
||||
f = open(self._filename, 'rb')
|
||||
content = f.read()
|
||||
f.close()
|
||||
except IOError:
|
||||
return credentials
|
||||
|
||||
try:
|
||||
credentials = client.Credentials.new_from_json(content)
|
||||
credentials.set_store(self)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return credentials
|
||||
|
||||
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._filename):
|
||||
old_umask = os.umask(0o177)
|
||||
try:
|
||||
open(self._filename, 'a+b').close()
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Write Credentials to file.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
|
||||
Raises:
|
||||
CredentialsFileSymbolicLinkError if the file is a symbolic link.
|
||||
"""
|
||||
self._create_file_if_needed()
|
||||
self._validate_file()
|
||||
f = open(self._filename, 'w')
|
||||
f.write(credentials.to_json())
|
||||
f.close()
|
||||
|
||||
def locked_delete(self):
|
||||
"""Delete Credentials file.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
os.unlink(self._filename)
|
673
venv/Lib/site-packages/oauth2client/service_account.py
Normal file
673
venv/Lib/site-packages/oauth2client/service_account.py
Normal file
|
@ -0,0 +1,673 @@
|
|||
# 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.
|
||||
|
||||
"""oauth2client Service account credentials class."""
|
||||
|
||||
import base64
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import time
|
||||
|
||||
import oauth2client
|
||||
from oauth2client import _helpers
|
||||
from oauth2client import client
|
||||
from oauth2client import crypt
|
||||
from oauth2client import transport
|
||||
from oauth2client import util
|
||||
|
||||
|
||||
_PASSWORD_DEFAULT = 'notasecret'
|
||||
_PKCS12_KEY = '_private_key_pkcs12'
|
||||
_PKCS12_ERROR = r"""
|
||||
This library only implements PKCS#12 support via the pyOpenSSL library.
|
||||
Either install pyOpenSSL, or please convert the .p12 file
|
||||
to .pem format:
|
||||
$ cat key.p12 | \
|
||||
> openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
|
||||
> openssl rsa > key.pem
|
||||
"""
|
||||
|
||||
|
||||
class ServiceAccountCredentials(client.AssertionCredentials):
|
||||
"""Service Account credential for OAuth 2.0 signed JWT grants.
|
||||
|
||||
Supports
|
||||
|
||||
* JSON keyfile (typically contains a PKCS8 key stored as
|
||||
PEM text)
|
||||
* ``.p12`` key (stores PKCS12 key and certificate)
|
||||
|
||||
Makes an assertion to server using a signed JWT assertion in exchange
|
||||
for an access token.
|
||||
|
||||
This credential does not require a flow to instantiate because it
|
||||
represents a two legged flow, and therefore has all of the required
|
||||
information to generate and refresh its own access tokens.
|
||||
|
||||
Args:
|
||||
service_account_email: string, The email associated with the
|
||||
service account.
|
||||
signer: ``crypt.Signer``, A signer which can be used to sign content.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring
|
||||
an access token.
|
||||
private_key_id: string, (Optional) Private key identifier. Typically
|
||||
only used with a JSON keyfile. Can be sent in the
|
||||
header of a JWT token assertion.
|
||||
client_id: string, (Optional) Client ID for the project that owns the
|
||||
service account.
|
||||
user_agent: string, (Optional) User agent to use when sending
|
||||
request.
|
||||
token_uri: string, URI for token endpoint. For convenience defaults
|
||||
to Google's endpoints but any OAuth 2.0 provider can be
|
||||
used.
|
||||
revoke_uri: string, URI for revoke endpoint. For convenience defaults
|
||||
to Google's endpoints but any OAuth 2.0 provider can be
|
||||
used.
|
||||
kwargs: dict, Extra key-value pairs (both strings) to send in the
|
||||
payload body when making an assertion.
|
||||
"""
|
||||
|
||||
MAX_TOKEN_LIFETIME_SECS = 3600
|
||||
"""Max lifetime of the token (one hour, in seconds)."""
|
||||
|
||||
NON_SERIALIZED_MEMBERS = (
|
||||
frozenset(['_signer']) |
|
||||
client.AssertionCredentials.NON_SERIALIZED_MEMBERS)
|
||||
"""Members that aren't serialized when object is converted to JSON."""
|
||||
|
||||
# Can be over-ridden by factory constructors. Used for
|
||||
# serialization/deserialization purposes.
|
||||
_private_key_pkcs8_pem = None
|
||||
_private_key_pkcs12 = None
|
||||
_private_key_password = None
|
||||
|
||||
def __init__(self,
|
||||
service_account_email,
|
||||
signer,
|
||||
scopes='',
|
||||
private_key_id=None,
|
||||
client_id=None,
|
||||
user_agent=None,
|
||||
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
|
||||
**kwargs):
|
||||
|
||||
super(ServiceAccountCredentials, self).__init__(
|
||||
None, user_agent=user_agent, token_uri=token_uri,
|
||||
revoke_uri=revoke_uri)
|
||||
|
||||
self._service_account_email = service_account_email
|
||||
self._signer = signer
|
||||
self._scopes = util.scopes_to_string(scopes)
|
||||
self._private_key_id = private_key_id
|
||||
self.client_id = client_id
|
||||
self._user_agent = user_agent
|
||||
self._kwargs = kwargs
|
||||
|
||||
def _to_json(self, strip, to_serialize=None):
|
||||
"""Utility function that creates JSON repr. of a credentials object.
|
||||
|
||||
Over-ride is needed since PKCS#12 keys will not in general be JSON
|
||||
serializable.
|
||||
|
||||
Args:
|
||||
strip: array, An array of names of members to exclude from the
|
||||
JSON.
|
||||
to_serialize: dict, (Optional) The properties for this object
|
||||
that will be serialized. This allows callers to
|
||||
modify before serializing.
|
||||
|
||||
Returns:
|
||||
string, a JSON representation of this instance, suitable to pass to
|
||||
from_json().
|
||||
"""
|
||||
if to_serialize is None:
|
||||
to_serialize = copy.copy(self.__dict__)
|
||||
pkcs12_val = to_serialize.get(_PKCS12_KEY)
|
||||
if pkcs12_val is not None:
|
||||
to_serialize[_PKCS12_KEY] = base64.b64encode(pkcs12_val)
|
||||
return super(ServiceAccountCredentials, self)._to_json(
|
||||
strip, to_serialize=to_serialize)
|
||||
|
||||
@classmethod
|
||||
def _from_parsed_json_keyfile(cls, keyfile_dict, scopes,
|
||||
token_uri=None, revoke_uri=None):
|
||||
"""Helper for factory constructors from JSON keyfile.
|
||||
|
||||
Args:
|
||||
keyfile_dict: dict-like object, The parsed dictionary-like object
|
||||
containing the contents of the JSON keyfile.
|
||||
scopes: List or string, Scopes to use when acquiring an
|
||||
access token.
|
||||
token_uri: string, URI for OAuth 2.0 provider token endpoint.
|
||||
If unset and not present in keyfile_dict, defaults
|
||||
to Google's endpoints.
|
||||
revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
|
||||
If unset and not present in keyfile_dict, defaults
|
||||
to Google's endpoints.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile contents.
|
||||
|
||||
Raises:
|
||||
ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
|
||||
KeyError, if one of the expected keys is not present in
|
||||
the keyfile.
|
||||
"""
|
||||
creds_type = keyfile_dict.get('type')
|
||||
if creds_type != client.SERVICE_ACCOUNT:
|
||||
raise ValueError('Unexpected credentials type', creds_type,
|
||||
'Expected', client.SERVICE_ACCOUNT)
|
||||
|
||||
service_account_email = keyfile_dict['client_email']
|
||||
private_key_pkcs8_pem = keyfile_dict['private_key']
|
||||
private_key_id = keyfile_dict['private_key_id']
|
||||
client_id = keyfile_dict['client_id']
|
||||
if not token_uri:
|
||||
token_uri = keyfile_dict.get('token_uri',
|
||||
oauth2client.GOOGLE_TOKEN_URI)
|
||||
if not revoke_uri:
|
||||
revoke_uri = keyfile_dict.get('revoke_uri',
|
||||
oauth2client.GOOGLE_REVOKE_URI)
|
||||
|
||||
signer = crypt.Signer.from_string(private_key_pkcs8_pem)
|
||||
credentials = cls(service_account_email, signer, scopes=scopes,
|
||||
private_key_id=private_key_id,
|
||||
client_id=client_id, token_uri=token_uri,
|
||||
revoke_uri=revoke_uri)
|
||||
credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
|
||||
return credentials
|
||||
|
||||
@classmethod
|
||||
def from_json_keyfile_name(cls, filename, scopes='',
|
||||
token_uri=None, revoke_uri=None):
|
||||
|
||||
"""Factory constructor from JSON keyfile by name.
|
||||
|
||||
Args:
|
||||
filename: string, The location of the keyfile.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring an
|
||||
access token.
|
||||
token_uri: string, URI for OAuth 2.0 provider token endpoint.
|
||||
If unset and not present in the key file, defaults
|
||||
to Google's endpoints.
|
||||
revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
|
||||
If unset and not present in the key file, defaults
|
||||
to Google's endpoints.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile.
|
||||
|
||||
Raises:
|
||||
ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
|
||||
KeyError, if one of the expected keys is not present in
|
||||
the keyfile.
|
||||
"""
|
||||
with open(filename, 'r') as file_obj:
|
||||
client_credentials = json.load(file_obj)
|
||||
return cls._from_parsed_json_keyfile(client_credentials, scopes,
|
||||
token_uri=token_uri,
|
||||
revoke_uri=revoke_uri)
|
||||
|
||||
@classmethod
|
||||
def from_json_keyfile_dict(cls, keyfile_dict, scopes='',
|
||||
token_uri=None, revoke_uri=None):
|
||||
"""Factory constructor from parsed JSON keyfile.
|
||||
|
||||
Args:
|
||||
keyfile_dict: dict-like object, The parsed dictionary-like object
|
||||
containing the contents of the JSON keyfile.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring an
|
||||
access token.
|
||||
token_uri: string, URI for OAuth 2.0 provider token endpoint.
|
||||
If unset and not present in keyfile_dict, defaults
|
||||
to Google's endpoints.
|
||||
revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
|
||||
If unset and not present in keyfile_dict, defaults
|
||||
to Google's endpoints.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile.
|
||||
|
||||
Raises:
|
||||
ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
|
||||
KeyError, if one of the expected keys is not present in
|
||||
the keyfile.
|
||||
"""
|
||||
return cls._from_parsed_json_keyfile(keyfile_dict, scopes,
|
||||
token_uri=token_uri,
|
||||
revoke_uri=revoke_uri)
|
||||
|
||||
@classmethod
|
||||
def _from_p12_keyfile_contents(cls, service_account_email,
|
||||
private_key_pkcs12,
|
||||
private_key_password=None, scopes='',
|
||||
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
|
||||
"""Factory constructor from JSON keyfile.
|
||||
|
||||
Args:
|
||||
service_account_email: string, The email associated with the
|
||||
service account.
|
||||
private_key_pkcs12: string, The contents of a PKCS#12 keyfile.
|
||||
private_key_password: string, (Optional) Password for PKCS#12
|
||||
private key. Defaults to ``notasecret``.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring an
|
||||
access token.
|
||||
token_uri: string, URI for token endpoint. For convenience defaults
|
||||
to Google's endpoints but any OAuth 2.0 provider can be
|
||||
used.
|
||||
revoke_uri: string, URI for revoke endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0
|
||||
provider can be used.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile.
|
||||
|
||||
Raises:
|
||||
NotImplementedError if pyOpenSSL is not installed / not the
|
||||
active crypto library.
|
||||
"""
|
||||
if private_key_password is None:
|
||||
private_key_password = _PASSWORD_DEFAULT
|
||||
if crypt.Signer is not crypt.OpenSSLSigner:
|
||||
raise NotImplementedError(_PKCS12_ERROR)
|
||||
signer = crypt.Signer.from_string(private_key_pkcs12,
|
||||
private_key_password)
|
||||
credentials = cls(service_account_email, signer, scopes=scopes,
|
||||
token_uri=token_uri, revoke_uri=revoke_uri)
|
||||
credentials._private_key_pkcs12 = private_key_pkcs12
|
||||
credentials._private_key_password = private_key_password
|
||||
return credentials
|
||||
|
||||
@classmethod
|
||||
def from_p12_keyfile(cls, service_account_email, filename,
|
||||
private_key_password=None, scopes='',
|
||||
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
|
||||
|
||||
"""Factory constructor from JSON keyfile.
|
||||
|
||||
Args:
|
||||
service_account_email: string, The email associated with the
|
||||
service account.
|
||||
filename: string, The location of the PKCS#12 keyfile.
|
||||
private_key_password: string, (Optional) Password for PKCS#12
|
||||
private key. Defaults to ``notasecret``.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring an
|
||||
access token.
|
||||
token_uri: string, URI for token endpoint. For convenience defaults
|
||||
to Google's endpoints but any OAuth 2.0 provider can be
|
||||
used.
|
||||
revoke_uri: string, URI for revoke endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0
|
||||
provider can be used.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile.
|
||||
|
||||
Raises:
|
||||
NotImplementedError if pyOpenSSL is not installed / not the
|
||||
active crypto library.
|
||||
"""
|
||||
with open(filename, 'rb') as file_obj:
|
||||
private_key_pkcs12 = file_obj.read()
|
||||
return cls._from_p12_keyfile_contents(
|
||||
service_account_email, private_key_pkcs12,
|
||||
private_key_password=private_key_password, scopes=scopes,
|
||||
token_uri=token_uri, revoke_uri=revoke_uri)
|
||||
|
||||
@classmethod
|
||||
def from_p12_keyfile_buffer(cls, service_account_email, file_buffer,
|
||||
private_key_password=None, scopes='',
|
||||
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
|
||||
"""Factory constructor from JSON keyfile.
|
||||
|
||||
Args:
|
||||
service_account_email: string, The email associated with the
|
||||
service account.
|
||||
file_buffer: stream, A buffer that implements ``read()``
|
||||
and contains the PKCS#12 key contents.
|
||||
private_key_password: string, (Optional) Password for PKCS#12
|
||||
private key. Defaults to ``notasecret``.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring an
|
||||
access token.
|
||||
token_uri: string, URI for token endpoint. For convenience defaults
|
||||
to Google's endpoints but any OAuth 2.0 provider can be
|
||||
used.
|
||||
revoke_uri: string, URI for revoke endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0
|
||||
provider can be used.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile.
|
||||
|
||||
Raises:
|
||||
NotImplementedError if pyOpenSSL is not installed / not the
|
||||
active crypto library.
|
||||
"""
|
||||
private_key_pkcs12 = file_buffer.read()
|
||||
return cls._from_p12_keyfile_contents(
|
||||
service_account_email, private_key_pkcs12,
|
||||
private_key_password=private_key_password, scopes=scopes,
|
||||
token_uri=token_uri, revoke_uri=revoke_uri)
|
||||
|
||||
def _generate_assertion(self):
|
||||
"""Generate the assertion that will be used in the request."""
|
||||
now = int(time.time())
|
||||
payload = {
|
||||
'aud': self.token_uri,
|
||||
'scope': self._scopes,
|
||||
'iat': now,
|
||||
'exp': now + self.MAX_TOKEN_LIFETIME_SECS,
|
||||
'iss': self._service_account_email,
|
||||
}
|
||||
payload.update(self._kwargs)
|
||||
return crypt.make_signed_jwt(self._signer, payload,
|
||||
key_id=self._private_key_id)
|
||||
|
||||
def sign_blob(self, blob):
|
||||
"""Cryptographically sign a blob (of bytes).
|
||||
|
||||
Implements abstract method
|
||||
:meth:`oauth2client.client.AssertionCredentials.sign_blob`.
|
||||
|
||||
Args:
|
||||
blob: bytes, Message to be signed.
|
||||
|
||||
Returns:
|
||||
tuple, A pair of the private key ID used to sign the blob and
|
||||
the signed contents.
|
||||
"""
|
||||
return self._private_key_id, self._signer.sign(blob)
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
"""Get the email for the current service account.
|
||||
|
||||
Returns:
|
||||
string, The email associated with the service account.
|
||||
"""
|
||||
return self._service_account_email
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
# NOTE: This is only useful for JSON keyfile.
|
||||
return {
|
||||
'type': 'service_account',
|
||||
'client_email': self._service_account_email,
|
||||
'private_key_id': self._private_key_id,
|
||||
'private_key': self._private_key_pkcs8_pem,
|
||||
'client_id': self.client_id,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
"""Deserialize a JSON-serialized instance.
|
||||
|
||||
Inverse to :meth:`to_json`.
|
||||
|
||||
Args:
|
||||
json_data: dict or string, Serialized JSON (as a string or an
|
||||
already parsed dictionary) representing a credential.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials from the serialized data.
|
||||
"""
|
||||
if not isinstance(json_data, dict):
|
||||
json_data = json.loads(_helpers._from_bytes(json_data))
|
||||
|
||||
private_key_pkcs8_pem = None
|
||||
pkcs12_val = json_data.get(_PKCS12_KEY)
|
||||
password = None
|
||||
if pkcs12_val is None:
|
||||
private_key_pkcs8_pem = json_data['_private_key_pkcs8_pem']
|
||||
signer = crypt.Signer.from_string(private_key_pkcs8_pem)
|
||||
else:
|
||||
# NOTE: This assumes that private_key_pkcs8_pem is not also
|
||||
# in the serialized data. This would be very incorrect
|
||||
# state.
|
||||
pkcs12_val = base64.b64decode(pkcs12_val)
|
||||
password = json_data['_private_key_password']
|
||||
signer = crypt.Signer.from_string(pkcs12_val, password)
|
||||
|
||||
credentials = cls(
|
||||
json_data['_service_account_email'],
|
||||
signer,
|
||||
scopes=json_data['_scopes'],
|
||||
private_key_id=json_data['_private_key_id'],
|
||||
client_id=json_data['client_id'],
|
||||
user_agent=json_data['_user_agent'],
|
||||
**json_data['_kwargs']
|
||||
)
|
||||
if private_key_pkcs8_pem is not None:
|
||||
credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
|
||||
if pkcs12_val is not None:
|
||||
credentials._private_key_pkcs12 = pkcs12_val
|
||||
if password is not None:
|
||||
credentials._private_key_password = password
|
||||
credentials.invalid = json_data['invalid']
|
||||
credentials.access_token = json_data['access_token']
|
||||
credentials.token_uri = json_data['token_uri']
|
||||
credentials.revoke_uri = json_data['revoke_uri']
|
||||
token_expiry = json_data.get('token_expiry', None)
|
||||
if token_expiry is not None:
|
||||
credentials.token_expiry = datetime.datetime.strptime(
|
||||
token_expiry, client.EXPIRY_FORMAT)
|
||||
return credentials
|
||||
|
||||
def create_scoped_required(self):
|
||||
return not self._scopes
|
||||
|
||||
def create_scoped(self, scopes):
|
||||
result = self.__class__(self._service_account_email,
|
||||
self._signer,
|
||||
scopes=scopes,
|
||||
private_key_id=self._private_key_id,
|
||||
client_id=self.client_id,
|
||||
user_agent=self._user_agent,
|
||||
**self._kwargs)
|
||||
result.token_uri = self.token_uri
|
||||
result.revoke_uri = self.revoke_uri
|
||||
result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
|
||||
result._private_key_pkcs12 = self._private_key_pkcs12
|
||||
result._private_key_password = self._private_key_password
|
||||
return result
|
||||
|
||||
def create_with_claims(self, claims):
|
||||
"""Create credentials that specify additional claims.
|
||||
|
||||
Args:
|
||||
claims: dict, key-value pairs for claims.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a copy of the current service account
|
||||
credentials with updated claims to use when obtaining access
|
||||
tokens.
|
||||
"""
|
||||
new_kwargs = dict(self._kwargs)
|
||||
new_kwargs.update(claims)
|
||||
result = self.__class__(self._service_account_email,
|
||||
self._signer,
|
||||
scopes=self._scopes,
|
||||
private_key_id=self._private_key_id,
|
||||
client_id=self.client_id,
|
||||
user_agent=self._user_agent,
|
||||
**new_kwargs)
|
||||
result.token_uri = self.token_uri
|
||||
result.revoke_uri = self.revoke_uri
|
||||
result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
|
||||
result._private_key_pkcs12 = self._private_key_pkcs12
|
||||
result._private_key_password = self._private_key_password
|
||||
return result
|
||||
|
||||
def create_delegated(self, sub):
|
||||
"""Create credentials that act as domain-wide delegation of authority.
|
||||
|
||||
Use the ``sub`` parameter as the subject to delegate on behalf of
|
||||
that user.
|
||||
|
||||
For example::
|
||||
|
||||
>>> account_sub = 'foo@email.com'
|
||||
>>> delegate_creds = creds.create_delegated(account_sub)
|
||||
|
||||
Args:
|
||||
sub: string, An email address that this service account will
|
||||
act on behalf of (via domain-wide delegation).
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a copy of the current service account
|
||||
updated to act on behalf of ``sub``.
|
||||
"""
|
||||
return self.create_with_claims({'sub': sub})
|
||||
|
||||
|
||||
def _datetime_to_secs(utc_time):
|
||||
# TODO(issue 298): use time_delta.total_seconds()
|
||||
# time_delta.total_seconds() not supported in Python 2.6
|
||||
epoch = datetime.datetime(1970, 1, 1)
|
||||
time_delta = utc_time - epoch
|
||||
return time_delta.days * 86400 + time_delta.seconds
|
||||
|
||||
|
||||
class _JWTAccessCredentials(ServiceAccountCredentials):
|
||||
"""Self signed JWT credentials.
|
||||
|
||||
Makes an assertion to server using a self signed JWT from service account
|
||||
credentials. These credentials do NOT use OAuth 2.0 and instead
|
||||
authenticate directly.
|
||||
"""
|
||||
_MAX_TOKEN_LIFETIME_SECS = 3600
|
||||
"""Max lifetime of the token (one hour, in seconds)."""
|
||||
|
||||
def __init__(self,
|
||||
service_account_email,
|
||||
signer,
|
||||
scopes=None,
|
||||
private_key_id=None,
|
||||
client_id=None,
|
||||
user_agent=None,
|
||||
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
|
||||
additional_claims=None):
|
||||
if additional_claims is None:
|
||||
additional_claims = {}
|
||||
super(_JWTAccessCredentials, self).__init__(
|
||||
service_account_email,
|
||||
signer,
|
||||
private_key_id=private_key_id,
|
||||
client_id=client_id,
|
||||
user_agent=user_agent,
|
||||
token_uri=token_uri,
|
||||
revoke_uri=revoke_uri,
|
||||
**additional_claims)
|
||||
|
||||
def authorize(self, http):
|
||||
"""Authorize an httplib2.Http instance with a JWT assertion.
|
||||
|
||||
Unless specified, the 'aud' of the assertion will be the base
|
||||
uri of the request.
|
||||
|
||||
Args:
|
||||
http: An instance of ``httplib2.Http`` or something that acts
|
||||
like it.
|
||||
Returns:
|
||||
A modified instance of http that was passed in.
|
||||
Example::
|
||||
h = httplib2.Http()
|
||||
h = credentials.authorize(h)
|
||||
"""
|
||||
transport.wrap_http_for_jwt_access(self, http)
|
||||
return http
|
||||
|
||||
def get_access_token(self, http=None, additional_claims=None):
|
||||
"""Create a signed jwt.
|
||||
|
||||
Args:
|
||||
http: unused
|
||||
additional_claims: dict, additional claims to add to
|
||||
the payload of the JWT.
|
||||
Returns:
|
||||
An AccessTokenInfo with the signed jwt
|
||||
"""
|
||||
if additional_claims is None:
|
||||
if self.access_token is None or self.access_token_expired:
|
||||
self.refresh(None)
|
||||
return client.AccessTokenInfo(
|
||||
access_token=self.access_token, expires_in=self._expires_in())
|
||||
else:
|
||||
# Create a 1 time token
|
||||
token, unused_expiry = self._create_token(additional_claims)
|
||||
return client.AccessTokenInfo(
|
||||
access_token=token, expires_in=self._MAX_TOKEN_LIFETIME_SECS)
|
||||
|
||||
def revoke(self, http):
|
||||
"""Cannot revoke JWTAccessCredentials tokens."""
|
||||
pass
|
||||
|
||||
def create_scoped_required(self):
|
||||
# JWTAccessCredentials are unscoped by definition
|
||||
return True
|
||||
|
||||
def create_scoped(self, scopes, token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
|
||||
# Returns an OAuth2 credentials with the given scope
|
||||
result = ServiceAccountCredentials(self._service_account_email,
|
||||
self._signer,
|
||||
scopes=scopes,
|
||||
private_key_id=self._private_key_id,
|
||||
client_id=self.client_id,
|
||||
user_agent=self._user_agent,
|
||||
token_uri=token_uri,
|
||||
revoke_uri=revoke_uri,
|
||||
**self._kwargs)
|
||||
if self._private_key_pkcs8_pem is not None:
|
||||
result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
|
||||
if self._private_key_pkcs12 is not None:
|
||||
result._private_key_pkcs12 = self._private_key_pkcs12
|
||||
if self._private_key_password is not None:
|
||||
result._private_key_password = self._private_key_password
|
||||
return result
|
||||
|
||||
def refresh(self, http):
|
||||
self._refresh(None)
|
||||
|
||||
def _refresh(self, http_request):
|
||||
self.access_token, self.token_expiry = self._create_token()
|
||||
|
||||
def _create_token(self, additional_claims=None):
|
||||
now = client._UTCNOW()
|
||||
lifetime = datetime.timedelta(seconds=self._MAX_TOKEN_LIFETIME_SECS)
|
||||
expiry = now + lifetime
|
||||
payload = {
|
||||
'iat': _datetime_to_secs(now),
|
||||
'exp': _datetime_to_secs(expiry),
|
||||
'iss': self._service_account_email,
|
||||
'sub': self._service_account_email
|
||||
}
|
||||
payload.update(self._kwargs)
|
||||
if additional_claims is not None:
|
||||
payload.update(additional_claims)
|
||||
jwt = crypt.make_signed_jwt(self._signer, payload,
|
||||
key_id=self._private_key_id)
|
||||
return jwt.decode('ascii'), expiry
|
256
venv/Lib/site-packages/oauth2client/tools.py
Normal file
256
venv/Lib/site-packages/oauth2client/tools.py
Normal file
|
@ -0,0 +1,256 @@
|
|||
# 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.
|
||||
|
||||
"""Command-line tools for authenticating via OAuth 2.0
|
||||
|
||||
Do the OAuth 2.0 Web Server dance for a command line application. Stores the
|
||||
generated credentials in a common file that is used by other example apps in
|
||||
the same directory.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from six.moves import BaseHTTPServer
|
||||
from six.moves import http_client
|
||||
from six.moves import input
|
||||
from six.moves import urllib
|
||||
|
||||
from oauth2client import client
|
||||
from oauth2client import util
|
||||
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
__all__ = ['argparser', 'run_flow', 'message_if_missing']
|
||||
|
||||
_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0
|
||||
|
||||
To make this sample run you will need to populate the client_secrets.json file
|
||||
found at:
|
||||
|
||||
{file_path}
|
||||
|
||||
with information from the APIs Console <https://code.google.com/apis/console>.
|
||||
|
||||
"""
|
||||
|
||||
_FAILED_START_MESSAGE = """
|
||||
Failed to start a local webserver listening on either port 8080
|
||||
or port 8090. Please check your firewall settings and locally
|
||||
running programs that may be blocking or using those ports.
|
||||
|
||||
Falling back to --noauth_local_webserver and continuing with
|
||||
authorization.
|
||||
"""
|
||||
|
||||
_BROWSER_OPENED_MESSAGE = """
|
||||
Your browser has been opened to visit:
|
||||
|
||||
{address}
|
||||
|
||||
If your browser is on a different machine then exit and re-run this
|
||||
application with the command-line parameter
|
||||
|
||||
--noauth_local_webserver
|
||||
"""
|
||||
|
||||
_GO_TO_LINK_MESSAGE = """
|
||||
Go to the following link in your browser:
|
||||
|
||||
{address}
|
||||
"""
|
||||
|
||||
|
||||
def _CreateArgumentParser():
|
||||
try:
|
||||
import argparse
|
||||
except ImportError: # pragma: NO COVER
|
||||
return None
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument('--auth_host_name', default='localhost',
|
||||
help='Hostname when running a local web server.')
|
||||
parser.add_argument('--noauth_local_webserver', action='store_true',
|
||||
default=False, help='Do not run a local web server.')
|
||||
parser.add_argument('--auth_host_port', default=[8080, 8090], type=int,
|
||||
nargs='*', help='Port web server should listen on.')
|
||||
parser.add_argument(
|
||||
'--logging_level', default='ERROR',
|
||||
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
||||
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.
|
||||
argparser = _CreateArgumentParser()
|
||||
|
||||
|
||||
class ClientRedirectServer(BaseHTTPServer.HTTPServer):
|
||||
"""A server to handle OAuth 2.0 redirects back to localhost.
|
||||
|
||||
Waits for a single request and parses the query parameters
|
||||
into query_params and then stops serving.
|
||||
"""
|
||||
query_params = {}
|
||||
|
||||
|
||||
class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
"""A handler for OAuth 2.0 redirects back to localhost.
|
||||
|
||||
Waits for a single request and parses the query parameters
|
||||
into the servers query_params and then stops serving.
|
||||
"""
|
||||
|
||||
def do_GET(self):
|
||||
"""Handle a GET request.
|
||||
|
||||
Parses the query parameters and prints a message
|
||||
if the flow has completed. Note that we can't detect
|
||||
if an error occurred.
|
||||
"""
|
||||
self.send_response(http_client.OK)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
query = self.path.split('?', 1)[-1]
|
||||
query = dict(urllib.parse.parse_qsl(query))
|
||||
self.server.query_params = query
|
||||
self.wfile.write(
|
||||
b"<html><head><title>Authentication Status</title></head>")
|
||||
self.wfile.write(
|
||||
b"<body><p>The authentication flow has completed.</p>")
|
||||
self.wfile.write(b"</body></html>")
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""Do not log messages to stdout while running as cmd. line program."""
|
||||
|
||||
|
||||
@util.positional(3)
|
||||
def run_flow(flow, storage, flags=None, http=None):
|
||||
"""Core code for a command-line application.
|
||||
|
||||
The ``run()`` function is called from your application and runs
|
||||
through all the steps to obtain credentials. It takes a ``Flow``
|
||||
argument and attempts to open an authorization server page in the
|
||||
user's default web browser. The server asks the user to grant your
|
||||
application access to the user's data. If the user grants access,
|
||||
the ``run()`` function returns new credentials. The new credentials
|
||||
are also stored in the ``storage`` argument, which updates the file
|
||||
associated with the ``Storage`` object.
|
||||
|
||||
It presumes it is run from a command-line application and supports the
|
||||
following flags:
|
||||
|
||||
``--auth_host_name`` (string, default: ``localhost``)
|
||||
Host name to use when running a local web server to handle
|
||||
redirects during OAuth authorization.
|
||||
|
||||
``--auth_host_port`` (integer, default: ``[8080, 8090]``)
|
||||
Port to use when running a local web server to handle redirects
|
||||
during OAuth authorization. Repeat this option to specify a list
|
||||
of values.
|
||||
|
||||
``--[no]auth_local_webserver`` (boolean, default: ``True``)
|
||||
Run a local web server to handle redirects during OAuth
|
||||
authorization.
|
||||
|
||||
The tools module defines an ``ArgumentParser`` the already contains the
|
||||
flag definitions that ``run()`` requires. You can pass that
|
||||
``ArgumentParser`` to your ``ArgumentParser`` constructor::
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
parents=[tools.argparser])
|
||||
flags = parser.parse_args(argv)
|
||||
|
||||
Args:
|
||||
flow: Flow, an OAuth 2.0 Flow to step through.
|
||||
storage: Storage, a ``Storage`` to store the credential in.
|
||||
flags: ``argparse.Namespace``, (Optional) The command-line flags. This
|
||||
is the object returned from calling ``parse_args()`` on
|
||||
``argparse.ArgumentParser`` as described above. Defaults
|
||||
to ``argparser.parse_args()``.
|
||||
http: An instance of ``httplib2.Http.request`` or something that
|
||||
acts like it.
|
||||
|
||||
Returns:
|
||||
Credentials, the obtained credential.
|
||||
"""
|
||||
if flags is None:
|
||||
flags = argparser.parse_args()
|
||||
logging.getLogger().setLevel(getattr(logging, flags.logging_level))
|
||||
if not flags.noauth_local_webserver:
|
||||
success = False
|
||||
port_number = 0
|
||||
for port in flags.auth_host_port:
|
||||
port_number = port
|
||||
try:
|
||||
httpd = ClientRedirectServer((flags.auth_host_name, port),
|
||||
ClientRedirectHandler)
|
||||
except socket.error:
|
||||
pass
|
||||
else:
|
||||
success = True
|
||||
break
|
||||
flags.noauth_local_webserver = not success
|
||||
if not success:
|
||||
print(_FAILED_START_MESSAGE)
|
||||
|
||||
if not flags.noauth_local_webserver:
|
||||
oauth_callback = 'http://{host}:{port}/'.format(
|
||||
host=flags.auth_host_name, port=port_number)
|
||||
else:
|
||||
oauth_callback = client.OOB_CALLBACK_URN
|
||||
flow.redirect_uri = oauth_callback
|
||||
authorize_url = flow.step1_get_authorize_url()
|
||||
|
||||
if not flags.noauth_local_webserver:
|
||||
import webbrowser
|
||||
webbrowser.open(authorize_url, new=1, autoraise=True)
|
||||
print(_BROWSER_OPENED_MESSAGE.format(address=authorize_url))
|
||||
else:
|
||||
print(_GO_TO_LINK_MESSAGE.format(address=authorize_url))
|
||||
|
||||
code = None
|
||||
if not flags.noauth_local_webserver:
|
||||
httpd.handle_request()
|
||||
if 'error' in httpd.query_params:
|
||||
sys.exit('Authentication request was rejected.')
|
||||
if 'code' in httpd.query_params:
|
||||
code = httpd.query_params['code']
|
||||
else:
|
||||
print('Failed to find "code" in the query parameters '
|
||||
'of the redirect.')
|
||||
sys.exit('Try running with --noauth_local_webserver.')
|
||||
else:
|
||||
code = input('Enter verification code: ').strip()
|
||||
|
||||
try:
|
||||
credential = flow.step2_exchange(code, http=http)
|
||||
except client.FlowExchangeError as e:
|
||||
sys.exit('Authentication has failed: {0}'.format(e))
|
||||
|
||||
storage.put(credential)
|
||||
credential.set_store(storage)
|
||||
print('Authentication successful.')
|
||||
|
||||
return credential
|
||||
|
||||
|
||||
def message_if_missing(filename):
|
||||
"""Helpful message to display if the CLIENT_SECRETS file is missing."""
|
||||
return _CLIENT_SECRETS_MESSAGE.format(file_path=filename)
|
245
venv/Lib/site-packages/oauth2client/transport.py
Normal file
245
venv/Lib/site-packages/oauth2client/transport.py
Normal file
|
@ -0,0 +1,245 @@
|
|||
# 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 logging
|
||||
|
||||
import httplib2
|
||||
import six
|
||||
from six.moves import http_client
|
||||
|
||||
from oauth2client._helpers import _to_bytes
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
# Properties present in file-like streams / buffers.
|
||||
_STREAM_PROPERTIES = ('read', 'seek', 'tell')
|
||||
|
||||
# Google Data client libraries may need to set this to [401, 403].
|
||||
REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,)
|
||||
|
||||
|
||||
class MemoryCache(object):
|
||||
"""httplib2 Cache implementation which only caches locally."""
|
||||
|
||||
def __init__(self):
|
||||
self.cache = {}
|
||||
|
||||
def get(self, key):
|
||||
return self.cache.get(key)
|
||||
|
||||
def set(self, key, value):
|
||||
self.cache[key] = value
|
||||
|
||||
def delete(self, key):
|
||||
self.cache.pop(key, None)
|
||||
|
||||
|
||||
def get_cached_http():
|
||||
"""Return an HTTP object which caches results returned.
|
||||
|
||||
This is intended to be used in methods like
|
||||
oauth2client.client.verify_id_token(), which calls to the same URI
|
||||
to retrieve certs.
|
||||
|
||||
Returns:
|
||||
httplib2.Http, an HTTP object with a MemoryCache
|
||||
"""
|
||||
return _CACHED_HTTP
|
||||
|
||||
|
||||
def get_http_object():
|
||||
"""Return a new HTTP object.
|
||||
|
||||
Returns:
|
||||
httplib2.Http, an HTTP object.
|
||||
"""
|
||||
return httplib2.Http()
|
||||
|
||||
|
||||
def _initialize_headers(headers):
|
||||
"""Creates a copy of the headers.
|
||||
|
||||
Args:
|
||||
headers: dict, request headers to copy.
|
||||
|
||||
Returns:
|
||||
dict, the copied headers or a new dictionary if the headers
|
||||
were None.
|
||||
"""
|
||||
return {} if headers is None else dict(headers)
|
||||
|
||||
|
||||
def _apply_user_agent(headers, user_agent):
|
||||
"""Adds a user-agent to the headers.
|
||||
|
||||
Args:
|
||||
headers: dict, request headers to add / modify user
|
||||
agent within.
|
||||
user_agent: str, the user agent to add.
|
||||
|
||||
Returns:
|
||||
dict, the original headers passed in, but modified if the
|
||||
user agent is not None.
|
||||
"""
|
||||
if user_agent is not None:
|
||||
if 'user-agent' in headers:
|
||||
headers['user-agent'] = (user_agent + ' ' + headers['user-agent'])
|
||||
else:
|
||||
headers['user-agent'] = user_agent
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
def clean_headers(headers):
|
||||
"""Forces header keys and values to be strings, i.e not unicode.
|
||||
|
||||
The httplib module just concats the header keys and values in a way that
|
||||
may make the message header a unicode string, which, if it then tries to
|
||||
contatenate to a binary request body may result in a unicode decode error.
|
||||
|
||||
Args:
|
||||
headers: dict, A dictionary of headers.
|
||||
|
||||
Returns:
|
||||
The same dictionary but with all the keys converted to strings.
|
||||
"""
|
||||
clean = {}
|
||||
try:
|
||||
for k, v in six.iteritems(headers):
|
||||
if not isinstance(k, six.binary_type):
|
||||
k = str(k)
|
||||
if not isinstance(v, six.binary_type):
|
||||
v = str(v)
|
||||
clean[_to_bytes(k)] = _to_bytes(v)
|
||||
except UnicodeEncodeError:
|
||||
from oauth2client.client import NonAsciiHeaderError
|
||||
raise NonAsciiHeaderError(k, ': ', v)
|
||||
return clean
|
||||
|
||||
|
||||
def wrap_http_for_auth(credentials, http):
|
||||
"""Prepares an HTTP object's request method for auth.
|
||||
|
||||
Wraps HTTP requests with logic to catch auth failures (typically
|
||||
identified via a 401 status code). In the event of failure, tries
|
||||
to refresh the token used and then retry the original request.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials used to identify
|
||||
the authenticated user.
|
||||
http: httplib2.Http, an http object to be used to make
|
||||
auth requests.
|
||||
"""
|
||||
orig_request_method = http.request
|
||||
|
||||
# The closure that will replace 'httplib2.Http.request'.
|
||||
def new_request(uri, method='GET', body=None, headers=None,
|
||||
redirections=httplib2.DEFAULT_MAX_REDIRECTS,
|
||||
connection_type=None):
|
||||
if not credentials.access_token:
|
||||
_LOGGER.info('Attempting refresh to obtain '
|
||||
'initial access_token')
|
||||
credentials._refresh(orig_request_method)
|
||||
|
||||
# Clone and modify the request headers to add the appropriate
|
||||
# Authorization header.
|
||||
headers = _initialize_headers(headers)
|
||||
credentials.apply(headers)
|
||||
_apply_user_agent(headers, credentials.user_agent)
|
||||
|
||||
body_stream_position = None
|
||||
# Check if the body is a file-like stream.
|
||||
if all(getattr(body, stream_prop, None) for stream_prop in
|
||||
_STREAM_PROPERTIES):
|
||||
body_stream_position = body.tell()
|
||||
|
||||
resp, content = orig_request_method(uri, method, body,
|
||||
clean_headers(headers),
|
||||
redirections, connection_type)
|
||||
|
||||
# A stored token may expire between the time it is retrieved and
|
||||
# the time the request is made, so we may need to try twice.
|
||||
max_refresh_attempts = 2
|
||||
for refresh_attempt in range(max_refresh_attempts):
|
||||
if resp.status not in REFRESH_STATUS_CODES:
|
||||
break
|
||||
_LOGGER.info('Refreshing due to a %s (attempt %s/%s)',
|
||||
resp.status, refresh_attempt + 1,
|
||||
max_refresh_attempts)
|
||||
credentials._refresh(orig_request_method)
|
||||
credentials.apply(headers)
|
||||
if body_stream_position is not None:
|
||||
body.seek(body_stream_position)
|
||||
|
||||
resp, content = orig_request_method(uri, method, body,
|
||||
clean_headers(headers),
|
||||
redirections, connection_type)
|
||||
|
||||
return resp, content
|
||||
|
||||
# Replace the request method with our own closure.
|
||||
http.request = new_request
|
||||
|
||||
# Set credentials as a property of the request method.
|
||||
setattr(http.request, 'credentials', credentials)
|
||||
|
||||
|
||||
def wrap_http_for_jwt_access(credentials, http):
|
||||
"""Prepares an HTTP object's request method for JWT access.
|
||||
|
||||
Wraps HTTP requests with logic to catch auth failures (typically
|
||||
identified via a 401 status code). In the event of failure, tries
|
||||
to refresh the token used and then retry the original request.
|
||||
|
||||
Args:
|
||||
credentials: _JWTAccessCredentials, the credentials used to identify
|
||||
a service account that uses JWT access tokens.
|
||||
http: httplib2.Http, an http object to be used to make
|
||||
auth requests.
|
||||
"""
|
||||
orig_request_method = http.request
|
||||
wrap_http_for_auth(credentials, http)
|
||||
# The new value of ``http.request`` set by ``wrap_http_for_auth``.
|
||||
authenticated_request_method = http.request
|
||||
|
||||
# The closure that will replace 'httplib2.Http.request'.
|
||||
def new_request(uri, method='GET', body=None, headers=None,
|
||||
redirections=httplib2.DEFAULT_MAX_REDIRECTS,
|
||||
connection_type=None):
|
||||
if 'aud' in credentials._kwargs:
|
||||
# Preemptively refresh token, this is not done for OAuth2
|
||||
if (credentials.access_token is None or
|
||||
credentials.access_token_expired):
|
||||
credentials.refresh(None)
|
||||
return authenticated_request_method(uri, method, body,
|
||||
headers, redirections,
|
||||
connection_type)
|
||||
else:
|
||||
# If we don't have an 'aud' (audience) claim,
|
||||
# create a 1-time token with the uri root as the audience
|
||||
headers = _initialize_headers(headers)
|
||||
_apply_user_agent(headers, credentials.user_agent)
|
||||
uri_root = uri.split('?', 1)[0]
|
||||
token, unused_expiry = credentials._create_token({'aud': uri_root})
|
||||
|
||||
headers['Authorization'] = 'Bearer ' + token
|
||||
return orig_request_method(uri, method, body,
|
||||
clean_headers(headers),
|
||||
redirections, connection_type)
|
||||
|
||||
# Replace the request method with our own closure.
|
||||
http.request = new_request
|
||||
|
||||
|
||||
_CACHED_HTTP = httplib2.Http(MemoryCache())
|
206
venv/Lib/site-packages/oauth2client/util.py
Normal file
206
venv/Lib/site-packages/oauth2client/util.py
Normal file
|
@ -0,0 +1,206 @@
|
|||
# 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)
|
Loading…
Add table
Add a link
Reference in a new issue