Add support for Google Default Credentials.
This commit is contained in:
@@ -164,6 +164,7 @@ class AppAssertionCredentials(AssertionCredentials):
|
|||||||
unspecified, the default service account for the app is used.
|
unspecified, the default service account for the app is used.
|
||||||
"""
|
"""
|
||||||
self.scope = util.scopes_to_string(scope)
|
self.scope = util.scopes_to_string(scope)
|
||||||
|
self._kwargs = kwargs
|
||||||
self.service_account_id = kwargs.get('service_account_id', None)
|
self.service_account_id = kwargs.get('service_account_id', None)
|
||||||
|
|
||||||
# Assertion type is no longer used, but still in the parent class signature.
|
# Assertion type is no longer used, but still in the parent class signature.
|
||||||
@@ -196,6 +197,12 @@ class AppAssertionCredentials(AssertionCredentials):
|
|||||||
raise AccessTokenRefreshError(str(e))
|
raise AccessTokenRefreshError(str(e))
|
||||||
self.access_token = token
|
self.access_token = token
|
||||||
|
|
||||||
|
def create_scoped_required(self):
|
||||||
|
return not self.scope
|
||||||
|
|
||||||
|
def create_scoped(self, scopes):
|
||||||
|
return AppAssertionCredentials(scopes, **self._kwargs)
|
||||||
|
|
||||||
|
|
||||||
class FlowProperty(db.Property):
|
class FlowProperty(db.Property):
|
||||||
"""App Engine datastore Property for Flow.
|
"""App Engine datastore Property for Flow.
|
||||||
|
|||||||
@@ -66,6 +66,14 @@ OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
|
|||||||
# Google Data client libraries may need to set this to [401, 403].
|
# Google Data client libraries may need to set this to [401, 403].
|
||||||
REFRESH_STATUS_CODES = [401]
|
REFRESH_STATUS_CODES = [401]
|
||||||
|
|
||||||
|
# The value representing user credentials.
|
||||||
|
AUTHORIZED_USER = 'authorized_user'
|
||||||
|
|
||||||
|
# The value representing service account credentials.
|
||||||
|
SERVICE_ACCOUNT = 'service_account'
|
||||||
|
|
||||||
|
# The environment variable pointing the file with local Default Credentials.
|
||||||
|
GOOGLE_CREDENTIALS_DEFAULT = 'GOOGLE_CREDENTIALS_DEFAULT'
|
||||||
|
|
||||||
class Error(Exception):
|
class Error(Exception):
|
||||||
"""Base error for this module."""
|
"""Base error for this module."""
|
||||||
@@ -99,6 +107,10 @@ class NonAsciiHeaderError(Error):
|
|||||||
"""Header names and values must be ASCII strings."""
|
"""Header names and values must be ASCII strings."""
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultCredentialsError(Error):
|
||||||
|
"""Error retrieving the Default Credentials."""
|
||||||
|
|
||||||
|
|
||||||
def _abstract():
|
def _abstract():
|
||||||
raise NotImplementedError('You need to override this function')
|
raise NotImplementedError('You need to override this function')
|
||||||
|
|
||||||
@@ -126,7 +138,7 @@ class Credentials(object):
|
|||||||
an HTTP transport.
|
an HTTP transport.
|
||||||
|
|
||||||
Subclasses must also specify a classmethod named 'from_json' that takes a JSON
|
Subclasses must also specify a classmethod named 'from_json' that takes a JSON
|
||||||
string as input and returns an instaniated Credentials object.
|
string as input and returns an instantiated Credentials object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
NON_SERIALIZED_MEMBERS = ['store']
|
NON_SERIALIZED_MEMBERS = ['store']
|
||||||
@@ -375,7 +387,7 @@ def _update_query_params(uri, params):
|
|||||||
The same URI but with the new query parameters added.
|
The same URI but with the new query parameters added.
|
||||||
"""
|
"""
|
||||||
parts = list(urlparse.urlparse(uri))
|
parts = list(urlparse.urlparse(uri))
|
||||||
query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part
|
query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part
|
||||||
query_params.update(params)
|
query_params.update(params)
|
||||||
parts[4] = urllib.urlencode(query_params)
|
parts[4] = urllib.urlencode(query_params)
|
||||||
return urlparse.urlunparse(parts)
|
return urlparse.urlunparse(parts)
|
||||||
@@ -587,6 +599,20 @@ class OAuth2Credentials(Credentials):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_access_token(self, http=None):
|
||||||
|
"""Return the access token.
|
||||||
|
|
||||||
|
If the token does not exist, get one.
|
||||||
|
If the token expired, refresh it.
|
||||||
|
"""
|
||||||
|
if self.access_token and not self.access_token_expired:
|
||||||
|
return self.access_token
|
||||||
|
else:
|
||||||
|
if not http:
|
||||||
|
http = httplib2.Http()
|
||||||
|
self.refresh(http)
|
||||||
|
return self.access_token
|
||||||
|
|
||||||
def set_store(self, store):
|
def set_store(self, store):
|
||||||
"""Set the Storage for the credential.
|
"""Set the Storage for the credential.
|
||||||
|
|
||||||
@@ -820,7 +846,303 @@ class AccessTokenCredentials(OAuth2Credentials):
|
|||||||
self._do_revoke(http_request, self.access_token)
|
self._do_revoke(http_request, self.access_token)
|
||||||
|
|
||||||
|
|
||||||
class AssertionCredentials(OAuth2Credentials):
|
_env_name = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_environment(urllib2_urlopen=None):
|
||||||
|
"""Detect the environment the code is being run on."""
|
||||||
|
|
||||||
|
global _env_name
|
||||||
|
|
||||||
|
if _env_name:
|
||||||
|
return _env_name
|
||||||
|
|
||||||
|
server_software = os.environ.get('SERVER_SOFTWARE', '')
|
||||||
|
if server_software.startswith('Google App Engine/'):
|
||||||
|
_env_name = 'GAE_PRODUCTION'
|
||||||
|
elif server_software.startswith('Development/'):
|
||||||
|
_env_name = 'GAE_LOCAL'
|
||||||
|
else:
|
||||||
|
import urllib2
|
||||||
|
try:
|
||||||
|
if urllib2_urlopen is None:
|
||||||
|
urllib2_urlopen = urllib2.urlopen
|
||||||
|
response = urllib2_urlopen('http://metadata.google.internal')
|
||||||
|
if any('Metadata-Flavor: Google' in h for h in response.info().headers):
|
||||||
|
_env_name = 'GCE_PRODUCTION'
|
||||||
|
else:
|
||||||
|
_env_name = 'UNKNOWN'
|
||||||
|
except urllib2.URLError:
|
||||||
|
_env_name = 'UNKNOWN'
|
||||||
|
|
||||||
|
return _env_name
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleCredentials(OAuth2Credentials):
|
||||||
|
"""Default credentials for use in calling Google APIs.
|
||||||
|
|
||||||
|
The Default Credentials are being constructed as a function of the environment
|
||||||
|
where the code is being run. More details can be found on this page:
|
||||||
|
https://developers.google.com/accounts/docs/default-credentials
|
||||||
|
|
||||||
|
Here is an example of how to use the Default Credentials for a service that
|
||||||
|
requires authentication:
|
||||||
|
|
||||||
|
<code>
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
from oauth2client.client import GoogleCredentials
|
||||||
|
|
||||||
|
PROJECT = 'bamboo-machine-422' # replace this with one of your projects
|
||||||
|
ZONE = 'us-central1-a' # replace this with the zone you care about
|
||||||
|
|
||||||
|
service = build('compute', 'v1', credentials=GoogleCredentials.get_default())
|
||||||
|
|
||||||
|
request = service.instances().list(project=PROJECT, zone=ZONE)
|
||||||
|
response = request.execute()
|
||||||
|
|
||||||
|
print response
|
||||||
|
</code>
|
||||||
|
|
||||||
|
A service that does not require authentication does not need credentials
|
||||||
|
to be passed in:
|
||||||
|
|
||||||
|
<code>
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
|
||||||
|
service = build('discovery', 'v1')
|
||||||
|
|
||||||
|
request = service.apis().list()
|
||||||
|
response = request.execute()
|
||||||
|
|
||||||
|
print response
|
||||||
|
</code>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, access_token, client_id, client_secret, refresh_token,
|
||||||
|
token_expiry, token_uri, user_agent,
|
||||||
|
revoke_uri=GOOGLE_REVOKE_URI):
|
||||||
|
"""Create an instance of GoogleCredentials.
|
||||||
|
|
||||||
|
This constructor is not usually called by the user, instead
|
||||||
|
GoogleCredentials objects are instantiated by
|
||||||
|
GoogleCredentials.from_stream() or GoogleCredentials.get_default().
|
||||||
|
|
||||||
|
Args:
|
||||||
|
access_token: string, access token.
|
||||||
|
client_id: string, client identifier.
|
||||||
|
client_secret: string, client secret.
|
||||||
|
refresh_token: string, refresh token.
|
||||||
|
token_expiry: datetime, when the access_token expires.
|
||||||
|
token_uri: string, URI of token endpoint.
|
||||||
|
user_agent: string, The HTTP User-Agent to provide for this application.
|
||||||
|
revoke_uri: string, URI for revoke endpoint.
|
||||||
|
Defaults to GOOGLE_REVOKE_URI; a token can't be revoked if this is None.
|
||||||
|
"""
|
||||||
|
super(GoogleCredentials, self).__init__(
|
||||||
|
access_token, client_id, client_secret, refresh_token, token_expiry,
|
||||||
|
token_uri, user_agent, revoke_uri=revoke_uri)
|
||||||
|
|
||||||
|
def create_scoped_required(self):
|
||||||
|
"""Whether this Credentials object is scopeless.
|
||||||
|
|
||||||
|
create_scoped(scopes) method needs to be called in order to create
|
||||||
|
a Credentials object for API calls.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_scoped(self, scopes):
|
||||||
|
"""Create a Credentials object for the given scopes.
|
||||||
|
|
||||||
|
The Credentials type is preserved.
|
||||||
|
"""
|
||||||
|
return self
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_default():
|
||||||
|
"""Get the Default Credentials for the current environment.
|
||||||
|
|
||||||
|
Exceptions:
|
||||||
|
DefaultCredentialsError: raised when the credentials fail to be retrieved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_env_name = _get_environment()
|
||||||
|
|
||||||
|
if _env_name in ('GAE_PRODUCTION', 'GAE_LOCAL'):
|
||||||
|
# if we are running inside Google App Engine
|
||||||
|
# there is no need to look for credentials in local files
|
||||||
|
default_credential_filename = None
|
||||||
|
well_known_file = None
|
||||||
|
else:
|
||||||
|
default_credential_filename = _get_environment_variable_file()
|
||||||
|
well_known_file = _get_well_known_file()
|
||||||
|
|
||||||
|
if default_credential_filename:
|
||||||
|
try:
|
||||||
|
return _get_default_credential_from_file(default_credential_filename)
|
||||||
|
except (DefaultCredentialsError, ValueError) as error:
|
||||||
|
extra_help = (' (pointed to by ' + GOOGLE_CREDENTIALS_DEFAULT +
|
||||||
|
' environment variable)')
|
||||||
|
_raise_exception_for_reading_json(default_credential_filename,
|
||||||
|
extra_help, error)
|
||||||
|
elif well_known_file:
|
||||||
|
try:
|
||||||
|
return _get_default_credential_from_file(well_known_file)
|
||||||
|
except (DefaultCredentialsError, ValueError) as error:
|
||||||
|
extra_help = (' (produced automatically when running'
|
||||||
|
' "gcloud auth login" command)')
|
||||||
|
_raise_exception_for_reading_json(well_known_file, extra_help, error)
|
||||||
|
elif _env_name in ('GAE_PRODUCTION', 'GAE_LOCAL'):
|
||||||
|
return _get_default_credential_GAE()
|
||||||
|
elif _env_name == 'GCE_PRODUCTION':
|
||||||
|
return _get_default_credential_GCE()
|
||||||
|
else:
|
||||||
|
raise DefaultCredentialsError(
|
||||||
|
"The Default Credentials are not available. They are available if "
|
||||||
|
"running in Google App Engine or Google Compute Engine. They are "
|
||||||
|
"also available if using the Google Cloud SDK and running 'gcloud "
|
||||||
|
"auth login'. Otherwise, the environment variable " +
|
||||||
|
GOOGLE_CREDENTIALS_DEFAULT + " must be defined pointing to a file "
|
||||||
|
"defining the credentials. "
|
||||||
|
"See https://developers.google.com/accounts/docs/default-credentials "
|
||||||
|
"for details.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_stream(credential_filename):
|
||||||
|
"""Create a Credentials object by reading the information from a given file.
|
||||||
|
|
||||||
|
It returns an object of type GoogleCredentials.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
credential_filename: the path to the file from where the credentials
|
||||||
|
are to be read
|
||||||
|
|
||||||
|
Exceptions:
|
||||||
|
DefaultCredentialsError: raised when the credentials fail to be retrieved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if credential_filename and os.path.isfile(credential_filename):
|
||||||
|
try:
|
||||||
|
return _get_default_credential_from_file(credential_filename)
|
||||||
|
except (DefaultCredentialsError, ValueError) as error:
|
||||||
|
extra_help = ' (provided as parameter to the from_stream() method)'
|
||||||
|
_raise_exception_for_reading_json(credential_filename,
|
||||||
|
extra_help,
|
||||||
|
error)
|
||||||
|
else:
|
||||||
|
raise DefaultCredentialsError('The parameter passed to the from_stream()'
|
||||||
|
' method should point to a file.')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_environment_variable_file():
|
||||||
|
default_credential_filename = os.environ.get(GOOGLE_CREDENTIALS_DEFAULT,
|
||||||
|
None)
|
||||||
|
|
||||||
|
if default_credential_filename:
|
||||||
|
if os.path.isfile(default_credential_filename):
|
||||||
|
return default_credential_filename
|
||||||
|
else:
|
||||||
|
raise DefaultCredentialsError(
|
||||||
|
'File ' + default_credential_filename + ' (pointed by ' +
|
||||||
|
GOOGLE_CREDENTIALS_DEFAULT + ' environment variable) does not exist!')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_well_known_file():
|
||||||
|
"""Get the well known file produced by command 'gcloud auth login'."""
|
||||||
|
# TODO(orestica): Revisit this method once gcloud provides a better way
|
||||||
|
# of pinpointing the exact location of the file.
|
||||||
|
|
||||||
|
WELL_KNOWN_CREDENTIALS_FILE = 'credentials_default.json'
|
||||||
|
CLOUDSDK_CONFIG_DIRECTORY = 'gcloud'
|
||||||
|
|
||||||
|
if os.name == 'nt':
|
||||||
|
try:
|
||||||
|
default_config_path = os.path.join(os.environ['APPDATA'],
|
||||||
|
CLOUDSDK_CONFIG_DIRECTORY)
|
||||||
|
except KeyError:
|
||||||
|
# This should never happen unless someone is really messing with things.
|
||||||
|
drive = os.environ.get('SystemDrive', 'C:')
|
||||||
|
default_config_path = os.path.join(drive, '\\', CLOUDSDK_CONFIG_DIRECTORY)
|
||||||
|
else:
|
||||||
|
default_config_path = os.path.join(os.path.expanduser('~'),
|
||||||
|
'.config',
|
||||||
|
CLOUDSDK_CONFIG_DIRECTORY)
|
||||||
|
|
||||||
|
default_config_path = os.path.join(default_config_path,
|
||||||
|
WELL_KNOWN_CREDENTIALS_FILE)
|
||||||
|
|
||||||
|
if os.path.isfile(default_config_path):
|
||||||
|
return default_config_path
|
||||||
|
|
||||||
|
|
||||||
|
def _get_default_credential_from_file(default_credential_filename):
|
||||||
|
"""Build the Default Credentials from file."""
|
||||||
|
|
||||||
|
import service_account
|
||||||
|
|
||||||
|
# read the credentials from the file
|
||||||
|
with open(default_credential_filename) as default_credential:
|
||||||
|
client_credentials = service_account.simplejson.load(default_credential)
|
||||||
|
|
||||||
|
credentials_type = client_credentials.get('type')
|
||||||
|
if credentials_type == AUTHORIZED_USER:
|
||||||
|
required_fields = set(['client_id', 'client_secret', 'refresh_token'])
|
||||||
|
elif credentials_type == SERVICE_ACCOUNT:
|
||||||
|
required_fields = set(['client_id', 'client_email', 'private_key_id',
|
||||||
|
'private_key'])
|
||||||
|
else:
|
||||||
|
raise DefaultCredentialsError("'type' field should be defined "
|
||||||
|
"(and have one of the '" + AUTHORIZED_USER +
|
||||||
|
"' or '" + SERVICE_ACCOUNT + "' values)")
|
||||||
|
|
||||||
|
missing_fields = required_fields.difference(client_credentials.keys())
|
||||||
|
|
||||||
|
if missing_fields:
|
||||||
|
_raise_exception_for_missing_fields(missing_fields)
|
||||||
|
|
||||||
|
if client_credentials['type'] == AUTHORIZED_USER:
|
||||||
|
return GoogleCredentials(
|
||||||
|
access_token=None,
|
||||||
|
client_id=client_credentials['client_id'],
|
||||||
|
client_secret=client_credentials['client_secret'],
|
||||||
|
refresh_token=client_credentials['refresh_token'],
|
||||||
|
token_expiry=None,
|
||||||
|
token_uri=GOOGLE_TOKEN_URI,
|
||||||
|
user_agent='Python client library')
|
||||||
|
else: # client_credentials['type'] == SERVICE_ACCOUNT
|
||||||
|
return service_account._ServiceAccountCredentials(
|
||||||
|
service_account_id=client_credentials['client_id'],
|
||||||
|
service_account_email=client_credentials['client_email'],
|
||||||
|
private_key_id=client_credentials['private_key_id'],
|
||||||
|
private_key_pkcs8_text=client_credentials['private_key'],
|
||||||
|
scopes=[])
|
||||||
|
|
||||||
|
|
||||||
|
def _raise_exception_for_missing_fields(missing_fields):
|
||||||
|
raise DefaultCredentialsError('The following field(s): ' +
|
||||||
|
', '.join(missing_fields) + ' must be defined.')
|
||||||
|
|
||||||
|
|
||||||
|
def _raise_exception_for_reading_json(credential_file,
|
||||||
|
extra_help,
|
||||||
|
error):
|
||||||
|
raise DefaultCredentialsError('An error was encountered while reading '
|
||||||
|
'json file: '+ credential_file + extra_help +
|
||||||
|
': ' + str(error))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_default_credential_GAE():
|
||||||
|
from oauth2client.appengine import AppAssertionCredentials
|
||||||
|
|
||||||
|
return AppAssertionCredentials([])
|
||||||
|
|
||||||
|
|
||||||
|
def _get_default_credential_GCE():
|
||||||
|
from oauth2client.gce import AppAssertionCredentials
|
||||||
|
|
||||||
|
return AppAssertionCredentials([])
|
||||||
|
|
||||||
|
|
||||||
|
class AssertionCredentials(GoogleCredentials):
|
||||||
"""Abstract Credentials object used for OAuth 2.0 assertion grants.
|
"""Abstract Credentials object used for OAuth 2.0 assertion grants.
|
||||||
|
|
||||||
This credential does not require a flow to instantiate because it
|
This credential does not require a flow to instantiate because it
|
||||||
@@ -899,7 +1221,7 @@ if HAS_CRYPTO:
|
|||||||
later. For App Engine you may also consider using AppAssertionCredentials.
|
later. For App Engine you may also consider using AppAssertionCredentials.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
|
MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
|
||||||
|
|
||||||
@util.positional(4)
|
@util.positional(4)
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class AppAssertionCredentials(AssertionCredentials):
|
|||||||
requested.
|
requested.
|
||||||
"""
|
"""
|
||||||
self.scope = util.scopes_to_string(scope)
|
self.scope = util.scopes_to_string(scope)
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
# Assertion type is no longer used, but still in the parent class signature.
|
# Assertion type is no longer used, but still in the parent class signature.
|
||||||
super(AppAssertionCredentials, self).__init__(None)
|
super(AppAssertionCredentials, self).__init__(None)
|
||||||
@@ -87,4 +88,14 @@ class AppAssertionCredentials(AssertionCredentials):
|
|||||||
raise AccessTokenRefreshError(str(e))
|
raise AccessTokenRefreshError(str(e))
|
||||||
self.access_token = d['accessToken']
|
self.access_token = d['accessToken']
|
||||||
else:
|
else:
|
||||||
|
if response.status == 404:
|
||||||
|
content = content + (' This can occur if a VM was created'
|
||||||
|
' with no service account or scopes.')
|
||||||
raise AccessTokenRefreshError(content)
|
raise AccessTokenRefreshError(content)
|
||||||
|
|
||||||
|
def create_scoped_required(self):
|
||||||
|
return not self.scope
|
||||||
|
|
||||||
|
def create_scoped(self, scopes):
|
||||||
|
return AppAssertionCredentials(scopes,
|
||||||
|
**self.kwargs)
|
||||||
|
|||||||
121
oauth2client/service_account.py
Normal file
121
oauth2client/service_account.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# Copyright (C) 2014 Google Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""A service account credentials class.
|
||||||
|
|
||||||
|
This credentials class is implemented on top of rsa library.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import rsa
|
||||||
|
import time
|
||||||
|
import types
|
||||||
|
|
||||||
|
from oauth2client import GOOGLE_REVOKE_URI
|
||||||
|
from oauth2client import GOOGLE_TOKEN_URI
|
||||||
|
from oauth2client import util
|
||||||
|
from oauth2client.anyjson import simplejson
|
||||||
|
from oauth2client.client import AssertionCredentials
|
||||||
|
|
||||||
|
from pyasn1.codec.ber import decoder
|
||||||
|
from pyasn1_modules.rfc5208 import PrivateKeyInfo
|
||||||
|
|
||||||
|
|
||||||
|
class _ServiceAccountCredentials(AssertionCredentials):
|
||||||
|
"""Class representing a service account (signed JWT) credential."""
|
||||||
|
|
||||||
|
MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
|
||||||
|
|
||||||
|
def __init__(self, service_account_id, service_account_email, private_key_id,
|
||||||
|
private_key_pkcs8_text, scopes, user_agent=None,
|
||||||
|
token_uri=GOOGLE_TOKEN_URI, revoke_uri=GOOGLE_REVOKE_URI, **kwargs):
|
||||||
|
|
||||||
|
super(_ServiceAccountCredentials, self).__init__(
|
||||||
|
None, user_agent=user_agent, token_uri=token_uri, revoke_uri=revoke_uri)
|
||||||
|
|
||||||
|
self._service_account_id = service_account_id
|
||||||
|
self._service_account_email = service_account_email
|
||||||
|
self._private_key_id = private_key_id
|
||||||
|
self._private_key = _get_private_key(private_key_pkcs8_text)
|
||||||
|
self._private_key_pkcs8_text = private_key_pkcs8_text
|
||||||
|
self._scopes = util.scopes_to_string(scopes)
|
||||||
|
self._user_agent = user_agent
|
||||||
|
self._token_uri = token_uri
|
||||||
|
self._revoke_uri = revoke_uri
|
||||||
|
self._kwargs = kwargs
|
||||||
|
|
||||||
|
def _generate_assertion(self):
|
||||||
|
"""Generate the assertion that will be used in the request."""
|
||||||
|
|
||||||
|
header = {
|
||||||
|
'alg': 'RS256',
|
||||||
|
'typ': 'JWT',
|
||||||
|
'kid': self._private_key_id
|
||||||
|
}
|
||||||
|
|
||||||
|
now = long(time.time())
|
||||||
|
payload = {
|
||||||
|
'aud': self._token_uri,
|
||||||
|
'scope': self._scopes,
|
||||||
|
'iat': now,
|
||||||
|
'exp': now + _ServiceAccountCredentials.MAX_TOKEN_LIFETIME_SECS,
|
||||||
|
'iss': self._service_account_email
|
||||||
|
}
|
||||||
|
payload.update(self._kwargs)
|
||||||
|
|
||||||
|
assertion_input = '%s.%s' % (
|
||||||
|
_urlsafe_b64encode(header),
|
||||||
|
_urlsafe_b64encode(payload))
|
||||||
|
|
||||||
|
# Sign the assertion.
|
||||||
|
signature = base64.urlsafe_b64encode(rsa.pkcs1.sign(
|
||||||
|
assertion_input, self._private_key, 'SHA-256')).rstrip('=')
|
||||||
|
|
||||||
|
return '%s.%s' % (assertion_input, signature)
|
||||||
|
|
||||||
|
def sign_blob(self, blob):
|
||||||
|
return (self._private_key_id,
|
||||||
|
rsa.pkcs1.sign(blob, self._private_key, 'SHA-256'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def service_account_email(self):
|
||||||
|
return self._service_account_email
|
||||||
|
|
||||||
|
def create_scoped_required(self):
|
||||||
|
return not self._scopes
|
||||||
|
|
||||||
|
def create_scoped(self, scopes):
|
||||||
|
return _ServiceAccountCredentials(self._service_account_id,
|
||||||
|
self._service_account_email,
|
||||||
|
self._private_key_id,
|
||||||
|
self._private_key_pkcs8_text,
|
||||||
|
scopes,
|
||||||
|
user_agent=self._user_agent,
|
||||||
|
token_uri=self._token_uri,
|
||||||
|
revoke_uri=self._revoke_uri,
|
||||||
|
**self._kwargs)
|
||||||
|
|
||||||
|
def _urlsafe_b64encode(data):
|
||||||
|
return base64.urlsafe_b64encode(
|
||||||
|
simplejson.dumps(data, separators = (',', ':'))\
|
||||||
|
.encode('UTF-8')).rstrip('=')
|
||||||
|
|
||||||
|
def _get_private_key(private_key_pkcs8_text):
|
||||||
|
"""Get an RSA private key object from a pkcs8 representation."""
|
||||||
|
|
||||||
|
der = rsa.pem.load_pem(private_key_pkcs8_text, 'PRIVATE KEY')
|
||||||
|
asn1_private_key, _ = decoder.decode(der, asn1Spec=PrivateKeyInfo())
|
||||||
|
return rsa.PrivateKey.load_pkcs1(
|
||||||
|
asn1_private_key.getComponentByName('privateKey').asOctets(),
|
||||||
|
format='DER')
|
||||||
15
samples/call_compute_service.py
Normal file
15
samples/call_compute_service.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# To be used to test GoogleCredential.GetDefaultCredential()
|
||||||
|
# from local machine and GCE.
|
||||||
|
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
from oauth2client.client import GoogleCredentials
|
||||||
|
|
||||||
|
PROJECT = "bamboo-machine-422" # Provide your own GCE project here
|
||||||
|
ZONE = "us-central1-a" # Put here a zone which has some VMs
|
||||||
|
|
||||||
|
service = build("compute", "v1", credentials=GoogleCredentials.get_default())
|
||||||
|
|
||||||
|
request = service.instances().list(project=PROJECT, zone=ZONE)
|
||||||
|
response = request.execute()
|
||||||
|
|
||||||
|
print response
|
||||||
10
samples/googleappengine/app.yaml
Normal file
10
samples/googleappengine/app.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
application: bamboo-machine-422
|
||||||
|
version: 2
|
||||||
|
runtime: python27
|
||||||
|
api_version: 1
|
||||||
|
threadsafe: true
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
- url: /.*
|
||||||
|
script: call_compute_service_from_gae.app
|
||||||
|
|
||||||
21
samples/googleappengine/call_compute_service_from_gae.py
Normal file
21
samples/googleappengine/call_compute_service_from_gae.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# To be used to test GoogleCredential.GetDefaultCredential()
|
||||||
|
# from devel GAE (ie, dev_appserver.py).
|
||||||
|
|
||||||
|
import webapp2
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
from oauth2client.client import GoogleCredentials
|
||||||
|
|
||||||
|
PROJECT = "bamboo-machine-422" # Provide your own GCE project here
|
||||||
|
ZONE = "us-central1-a" # Put here a zone which has some VMs
|
||||||
|
|
||||||
|
def get_instances():
|
||||||
|
service = build("compute", "v1", credentials=GoogleCredentials.get_default())
|
||||||
|
request = service.instances().list(project=PROJECT, zone=ZONE)
|
||||||
|
return request.execute()
|
||||||
|
|
||||||
|
class MainPage(webapp2.RequestHandler):
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
self.response.write(get_instances())
|
||||||
|
|
||||||
|
app = webapp2.WSGIApplication([('/', MainPage),], debug=True)
|
||||||
3
setup.py
3
setup.py
@@ -26,6 +26,9 @@ packages = [
|
|||||||
|
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'httplib2>=0.8',
|
'httplib2>=0.8',
|
||||||
|
'pyasn1==0.1.7',
|
||||||
|
'pyasn1_modules==0.0.5',
|
||||||
|
'rsa==3.1.4',
|
||||||
]
|
]
|
||||||
|
|
||||||
needs_json = False
|
needs_json = False
|
||||||
|
|||||||
9
tests/data/gcloud/credentials_default.json
Normal file
9
tests/data/gcloud/credentials_default.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"type": "service_account",
|
||||||
|
"client_id": "123",
|
||||||
|
"client_secret": "secret",
|
||||||
|
"refresh_token": "alabalaportocala",
|
||||||
|
"client_email": "dummy@google.com",
|
||||||
|
"private_key_id": "ABCDEF",
|
||||||
|
"private_key": "Bag Attributes\n friendlyName: key\n localKeyID: 22 7E 04 FC 64 48 20 83 1E C1 BD E3 F5 2F 44 7D EA 99 A5 BC\nKey Attributes: <No Attributes>\n-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDh6PSnttDsv+vi\ntUZTP1E3hVBah6PUGDWZhYgNiyW8quTWCmPvBmCR2YzuhUrY5+CtKP8UJOQico+p\noJHSAPsrzSr6YsGs3c9SQOslBmm9Fkh9/f/GZVTVZ6u5AsUmOcVvZ2q7Sz8Vj/aR\naIm0EJqRe9cQ5vvN9sg25rIv4xKwIZJ1VixKWJLmpCmDINqn7xvl+ldlUmSr3aGt\nw21uSDuEJhQlzO3yf2FwJMkJ9SkCm9oVDXyl77OnKXj5bOQ/rojbyGeIxDJSUDWE\nGKyRPuqKi6rSbwg6h2G/Z9qBJkqM5NNTbGRIFz/9/LdmmwvtaqCxlLtD7RVEryAp\n+qTGDk5hAgMBAAECggEBAMYYfNDEYpf4A2SdCLne/9zrrfZ0kphdUkL48MDPj5vN\nTzTRj6f9s5ixZ/+QKn3hdwbguCx13QbH5mocP0IjUhyqoFFHYAWxyyaZfpjM8tO4\nQoEYxby3BpjLe62UXESUzChQSytJZFwIDXKcdIPNO3zvVzufEJcfG5no2b9cIvsG\nDy6J1FNILWxCtDIqBM+G1B1is9DhZnUDgn0iKzINiZmh1I1l7k/4tMnozVIKAfwo\nf1kYjG/d2IzDM02mTeTElz3IKeNriaOIYTZgI26xLJxTkiFnBV4JOWFAZw15X+yR\n+DrjGSIkTfhzbLa20Vt3AFM+LFK0ZoXT2dRnjbYPjQECgYEA+9XJFGwLcEX6pl1p\nIwXAjXKJdju9DDn4lmHTW0Pbw25h1EXONwm/NPafwsWmPll9kW9IwsxUQVUyBC9a\nc3Q7rF1e8ai/qqVFRIZof275MI82ciV2Mw8Hz7FPAUyoju5CvnjAEH4+irt1VE/7\nSgdvQ1gDBQFegS69ijdz+cOhFxkCgYEA5aVoseMy/gIlsCvNPyw9+Jz/zBpKItX0\njGzdF7lhERRO2cursujKaoHntRckHcE3P/Z4K565bvVq+VaVG0T/BcBKPmPHrLmY\niuVXidltW7Jh9/RCVwb5+BvqlwlC470PEwhqoUatY/fPJ74srztrqJHvp1L29FT5\nsdmlJW8YwokCgYAUa3dMgp5C0knKp5RY1KSSU5E11w4zKZgwiWob4lq1dAPWtHpO\nGCo63yyBHImoUJVP75gUw4Cpc4EEudo5tlkIVuHV8nroGVKOhd9/Rb5K47Hke4kk\nBrn5a0Ues9qPDF65Fw1ryPDFSwHufjXAAO5SpZZJF51UGDgiNvDedbBgMQKBgHSk\nt7DjPhtW69234eCckD2fQS5ijBV1p2lMQmCygGM0dXiawvN02puOsCqDPoz+fxm2\nDwPY80cw0M0k9UeMnBxHt25JMDrDan/iTbxu++T/jlNrdebOXFlxlI5y3c7fULDS\nLZcNVzTXwhjlt7yp6d0NgzTyJw2ju9BiREfnTiRBAoGBAOPHrTOnPyjO+bVcCPTB\nWGLsbBd77mVPGIuL0XGrvbVYPE8yIcNbZcthd8VXL/38Ygy8SIZh2ZqsrU1b5WFa\nXUMLnGEODSS8x/GmW3i3KeirW5OxBNjfUzEF4XkJP8m41iTdsQEXQf9DdUY7X+CB\nVL5h7N0VstYhGgycuPpcIUQa\n-----END PRIVATE KEY-----\n"
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"type": "authorized_user",
|
||||||
|
"client_id": "123",
|
||||||
|
"client_secret": "secret",
|
||||||
|
"refresh_token": "alabalaportocala",
|
||||||
|
"client_email": "dummy@google.com",
|
||||||
|
"private_key_id": "ABCDEF",
|
||||||
|
"private_key": "Bag Attributes\n friendlyName: key\n localKeyID: 22 7E 04 FC 64 48 20 83 1E C1 BD E3 F5 2F 44 7D EA 99 A5 BC\nKey Attributes: <No Attributes>\n-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDh6PSnttDsv+vi\ntUZTP1E3hVBah6PUGDWZhYgNiyW8quTWCmPvBmCR2YzuhUrY5+CtKP8UJOQico+p\noJHSAPsrzSr6YsGs3c9SQOslBmm9Fkh9/f/GZVTVZ6u5AsUmOcVvZ2q7Sz8Vj/aR\naIm0EJqRe9cQ5vvN9sg25rIv4xKwIZJ1VixKWJLmpCmDINqn7xvl+ldlUmSr3aGt\nw21uSDuEJhQlzO3yf2FwJMkJ9SkCm9oVDXyl77OnKXj5bOQ/rojbyGeIxDJSUDWE\nGKyRPuqKi6rSbwg6h2G/Z9qBJkqM5NNTbGRIFz/9/LdmmwvtaqCxlLtD7RVEryAp\n+qTGDk5hAgMBAAECggEBAMYYfNDEYpf4A2SdCLne/9zrrfZ0kphdUkL48MDPj5vN\nTzTRj6f9s5ixZ/+QKn3hdwbguCx13QbH5mocP0IjUhyqoFFHYAWxyyaZfpjM8tO4\nQoEYxby3BpjLe62UXESUzChQSytJZFwIDXKcdIPNO3zvVzufEJcfG5no2b9cIvsG\nDy6J1FNILWxCtDIqBM+G1B1is9DhZnUDgn0iKzINiZmh1I1l7k/4tMnozVIKAfwo\nf1kYjG/d2IzDM02mTeTElz3IKeNriaOIYTZgI26xLJxTkiFnBV4JOWFAZw15X+yR\n+DrjGSIkTfhzbLa20Vt3AFM+LFK0ZoXT2dRnjbYPjQECgYEA+9XJFGwLcEX6pl1p\nIwXAjXKJdju9DDn4lmHTW0Pbw25h1EXONwm/NPafwsWmPll9kW9IwsxUQVUyBC9a\nc3Q7rF1e8ai/qqVFRIZof275MI82ciV2Mw8Hz7FPAUyoju5CvnjAEH4+irt1VE/7\nSgdvQ1gDBQFegS69ijdz+cOhFxkCgYEA5aVoseMy/gIlsCvNPyw9+Jz/zBpKItX0\njGzdF7lhERRO2cursujKaoHntRckHcE3P/Z4K565bvVq+VaVG0T/BcBKPmPHrLmY\niuVXidltW7Jh9/RCVwb5+BvqlwlC470PEwhqoUatY/fPJ74srztrqJHvp1L29FT5\nsdmlJW8YwokCgYAUa3dMgp5C0knKp5RY1KSSU5E11w4zKZgwiWob4lq1dAPWtHpO\nGCo63yyBHImoUJVP75gUw4Cpc4EEudo5tlkIVuHV8nroGVKOhd9/Rb5K47Hke4kk\nBrn5a0Ues9qPDF65Fw1ryPDFSwHufjXAAO5SpZZJF51UGDgiNvDedbBgMQKBgHSk\nt7DjPhtW69234eCckD2fQS5ijBV1p2lMQmCygGM0dXiawvN02puOsCqDPoz+fxm2\nDwPY80cw0M0k9UeMnBxHt25JMDrDan/iTbxu++T/jlNrdebOXFlxlI5y3c7fULDS\nLZcNVzTXwhjlt7yp6d0NgzTyJw2ju9BiREfnTiRBAoGBAOPHrTOnPyjO+bVcCPTB\nWGLsbBd77mVPGIuL0XGrvbVYPE8yIcNbZcthd8VXL/38Ygy8SIZh2ZqsrU1b5WFa\nXUMLnGEODSS8x/GmW3i3KeirW5OxBNjfUzEF4XkJP8m41iTdsQEXQf9DdUY7X+CB\nVL5h7N0VstYhGgycuPpcIUQa\n-----END PRIVATE KEY-----\n"
|
||||||
|
}
|
||||||
9
tests/data/gcloud/credentials_default_malformed_1.json
Normal file
9
tests/data/gcloud/credentials_default_malformed_1.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"type": "serviceaccount",
|
||||||
|
"client_id": "123",
|
||||||
|
"client_secret": "secret",
|
||||||
|
"refresh_token": "alabalaportocala",
|
||||||
|
"client_email": "dummy@google.com",
|
||||||
|
"private_key_id": "ABCDEF",
|
||||||
|
"private_key": "Bag Attributes\n friendlyName: key\n localKeyID: 22 7E 04 FC 64 48 20 83 1E C1 BD E3 F5 2F 44 7D EA 99 A5 BC\nKey Attributes: <No Attributes>\n-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDh6PSnttDsv+vi\ntUZTP1E3hVBah6PUGDWZhYgNiyW8quTWCmPvBmCR2YzuhUrY5+CtKP8UJOQico+p\noJHSAPsrzSr6YsGs3c9SQOslBmm9Fkh9/f/GZVTVZ6u5AsUmOcVvZ2q7Sz8Vj/aR\naIm0EJqRe9cQ5vvN9sg25rIv4xKwIZJ1VixKWJLmpCmDINqn7xvl+ldlUmSr3aGt\nw21uSDuEJhQlzO3yf2FwJMkJ9SkCm9oVDXyl77OnKXj5bOQ/rojbyGeIxDJSUDWE\nGKyRPuqKi6rSbwg6h2G/Z9qBJkqM5NNTbGRIFz/9/LdmmwvtaqCxlLtD7RVEryAp\n+qTGDk5hAgMBAAECggEBAMYYfNDEYpf4A2SdCLne/9zrrfZ0kphdUkL48MDPj5vN\nTzTRj6f9s5ixZ/+QKn3hdwbguCx13QbH5mocP0IjUhyqoFFHYAWxyyaZfpjM8tO4\nQoEYxby3BpjLe62UXESUzChQSytJZFwIDXKcdIPNO3zvVzufEJcfG5no2b9cIvsG\nDy6J1FNILWxCtDIqBM+G1B1is9DhZnUDgn0iKzINiZmh1I1l7k/4tMnozVIKAfwo\nf1kYjG/d2IzDM02mTeTElz3IKeNriaOIYTZgI26xLJxTkiFnBV4JOWFAZw15X+yR\n+DrjGSIkTfhzbLa20Vt3AFM+LFK0ZoXT2dRnjbYPjQECgYEA+9XJFGwLcEX6pl1p\nIwXAjXKJdju9DDn4lmHTW0Pbw25h1EXONwm/NPafwsWmPll9kW9IwsxUQVUyBC9a\nc3Q7rF1e8ai/qqVFRIZof275MI82ciV2Mw8Hz7FPAUyoju5CvnjAEH4+irt1VE/7\nSgdvQ1gDBQFegS69ijdz+cOhFxkCgYEA5aVoseMy/gIlsCvNPyw9+Jz/zBpKItX0\njGzdF7lhERRO2cursujKaoHntRckHcE3P/Z4K565bvVq+VaVG0T/BcBKPmPHrLmY\niuVXidltW7Jh9/RCVwb5+BvqlwlC470PEwhqoUatY/fPJ74srztrqJHvp1L29FT5\nsdmlJW8YwokCgYAUa3dMgp5C0knKp5RY1KSSU5E11w4zKZgwiWob4lq1dAPWtHpO\nGCo63yyBHImoUJVP75gUw4Cpc4EEudo5tlkIVuHV8nroGVKOhd9/Rb5K47Hke4kk\nBrn5a0Ues9qPDF65Fw1ryPDFSwHufjXAAO5SpZZJF51UGDgiNvDedbBgMQKBgHSk\nt7DjPhtW69234eCckD2fQS5ijBV1p2lMQmCygGM0dXiawvN02puOsCqDPoz+fxm2\nDwPY80cw0M0k9UeMnBxHt25JMDrDan/iTbxu++T/jlNrdebOXFlxlI5y3c7fULDS\nLZcNVzTXwhjlt7yp6d0NgzTyJw2ju9BiREfnTiRBAoGBAOPHrTOnPyjO+bVcCPTB\nWGLsbBd77mVPGIuL0XGrvbVYPE8yIcNbZcthd8VXL/38Ygy8SIZh2ZqsrU1b5WFa\nXUMLnGEODSS8x/GmW3i3KeirW5OxBNjfUzEF4XkJP8m41iTdsQEXQf9DdUY7X+CB\nVL5h7N0VstYhGgycuPpcIUQa\n-----END PRIVATE KEY-----\n"
|
||||||
|
}
|
||||||
8
tests/data/gcloud/credentials_default_malformed_2.json
Normal file
8
tests/data/gcloud/credentials_default_malformed_2.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"type": "service_account",
|
||||||
|
"client_id": "123",
|
||||||
|
"client_secret": "secret",
|
||||||
|
"refresh_token": "alabalaportocala",
|
||||||
|
"client_email": "dummy@google.com",
|
||||||
|
"private_key": "Bag Attributes\n friendlyName: key\n localKeyID: 22 7E 04 FC 64 48 20 83 1E C1 BD E3 F5 2F 44 7D EA 99 A5 BC\nKey Attributes: <No Attributes>\n-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDh6PSnttDsv+vi\ntUZTP1E3hVBah6PUGDWZhYgNiyW8quTWCmPvBmCR2YzuhUrY5+CtKP8UJOQico+p\noJHSAPsrzSr6YsGs3c9SQOslBmm9Fkh9/f/GZVTVZ6u5AsUmOcVvZ2q7Sz8Vj/aR\naIm0EJqRe9cQ5vvN9sg25rIv4xKwIZJ1VixKWJLmpCmDINqn7xvl+ldlUmSr3aGt\nw21uSDuEJhQlzO3yf2FwJMkJ9SkCm9oVDXyl77OnKXj5bOQ/rojbyGeIxDJSUDWE\nGKyRPuqKi6rSbwg6h2G/Z9qBJkqM5NNTbGRIFz/9/LdmmwvtaqCxlLtD7RVEryAp\n+qTGDk5hAgMBAAECggEBAMYYfNDEYpf4A2SdCLne/9zrrfZ0kphdUkL48MDPj5vN\nTzTRj6f9s5ixZ/+QKn3hdwbguCx13QbH5mocP0IjUhyqoFFHYAWxyyaZfpjM8tO4\nQoEYxby3BpjLe62UXESUzChQSytJZFwIDXKcdIPNO3zvVzufEJcfG5no2b9cIvsG\nDy6J1FNILWxCtDIqBM+G1B1is9DhZnUDgn0iKzINiZmh1I1l7k/4tMnozVIKAfwo\nf1kYjG/d2IzDM02mTeTElz3IKeNriaOIYTZgI26xLJxTkiFnBV4JOWFAZw15X+yR\n+DrjGSIkTfhzbLa20Vt3AFM+LFK0ZoXT2dRnjbYPjQECgYEA+9XJFGwLcEX6pl1p\nIwXAjXKJdju9DDn4lmHTW0Pbw25h1EXONwm/NPafwsWmPll9kW9IwsxUQVUyBC9a\nc3Q7rF1e8ai/qqVFRIZof275MI82ciV2Mw8Hz7FPAUyoju5CvnjAEH4+irt1VE/7\nSgdvQ1gDBQFegS69ijdz+cOhFxkCgYEA5aVoseMy/gIlsCvNPyw9+Jz/zBpKItX0\njGzdF7lhERRO2cursujKaoHntRckHcE3P/Z4K565bvVq+VaVG0T/BcBKPmPHrLmY\niuVXidltW7Jh9/RCVwb5+BvqlwlC470PEwhqoUatY/fPJ74srztrqJHvp1L29FT5\nsdmlJW8YwokCgYAUa3dMgp5C0knKp5RY1KSSU5E11w4zKZgwiWob4lq1dAPWtHpO\nGCo63yyBHImoUJVP75gUw4Cpc4EEudo5tlkIVuHV8nroGVKOhd9/Rb5K47Hke4kk\nBrn5a0Ues9qPDF65Fw1ryPDFSwHufjXAAO5SpZZJF51UGDgiNvDedbBgMQKBgHSk\nt7DjPhtW69234eCckD2fQS5ijBV1p2lMQmCygGM0dXiawvN02puOsCqDPoz+fxm2\nDwPY80cw0M0k9UeMnBxHt25JMDrDan/iTbxu++T/jlNrdebOXFlxlI5y3c7fULDS\nLZcNVzTXwhjlt7yp6d0NgzTyJw2ju9BiREfnTiRBAoGBAOPHrTOnPyjO+bVcCPTB\nWGLsbBd77mVPGIuL0XGrvbVYPE8yIcNbZcthd8VXL/38Ygy8SIZh2ZqsrU1b5WFa\nXUMLnGEODSS8x/GmW3i3KeirW5OxBNjfUzEF4XkJP8m41iTdsQEXQf9DdUY7X+CB\nVL5h7N0VstYhGgycuPpcIUQa\n-----END PRIVATE KEY-----\n"
|
||||||
|
}
|
||||||
9
tests/data/gcloud/credentials_default_malformed_3.json
Normal file
9
tests/data/gcloud/credentials_default_malformed_3.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"type": "service_account"
|
||||||
|
"client_id": "123",
|
||||||
|
"client_secret": "secret",
|
||||||
|
"refresh_token": "alabalaportocala",
|
||||||
|
"client_email": "dummy@google.com",
|
||||||
|
"private_key_id": "ABCDEF",
|
||||||
|
"private_key": "Bag Attributes\n friendlyName: key\n localKeyID: 22 7E 04 FC 64 48 20 83 1E C1 BD E3 F5 2F 44 7D EA 99 A5 BC\nKey Attributes: <No Attributes>\n-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDh6PSnttDsv+vi\ntUZTP1E3hVBah6PUGDWZhYgNiyW8quTWCmPvBmCR2YzuhUrY5+CtKP8UJOQico+p\noJHSAPsrzSr6YsGs3c9SQOslBmm9Fkh9/f/GZVTVZ6u5AsUmOcVvZ2q7Sz8Vj/aR\naIm0EJqRe9cQ5vvN9sg25rIv4xKwIZJ1VixKWJLmpCmDINqn7xvl+ldlUmSr3aGt\nw21uSDuEJhQlzO3yf2FwJMkJ9SkCm9oVDXyl77OnKXj5bOQ/rojbyGeIxDJSUDWE\nGKyRPuqKi6rSbwg6h2G/Z9qBJkqM5NNTbGRIFz/9/LdmmwvtaqCxlLtD7RVEryAp\n+qTGDk5hAgMBAAECggEBAMYYfNDEYpf4A2SdCLne/9zrrfZ0kphdUkL48MDPj5vN\nTzTRj6f9s5ixZ/+QKn3hdwbguCx13QbH5mocP0IjUhyqoFFHYAWxyyaZfpjM8tO4\nQoEYxby3BpjLe62UXESUzChQSytJZFwIDXKcdIPNO3zvVzufEJcfG5no2b9cIvsG\nDy6J1FNILWxCtDIqBM+G1B1is9DhZnUDgn0iKzINiZmh1I1l7k/4tMnozVIKAfwo\nf1kYjG/d2IzDM02mTeTElz3IKeNriaOIYTZgI26xLJxTkiFnBV4JOWFAZw15X+yR\n+DrjGSIkTfhzbLa20Vt3AFM+LFK0ZoXT2dRnjbYPjQECgYEA+9XJFGwLcEX6pl1p\nIwXAjXKJdju9DDn4lmHTW0Pbw25h1EXONwm/NPafwsWmPll9kW9IwsxUQVUyBC9a\nc3Q7rF1e8ai/qqVFRIZof275MI82ciV2Mw8Hz7FPAUyoju5CvnjAEH4+irt1VE/7\nSgdvQ1gDBQFegS69ijdz+cOhFxkCgYEA5aVoseMy/gIlsCvNPyw9+Jz/zBpKItX0\njGzdF7lhERRO2cursujKaoHntRckHcE3P/Z4K565bvVq+VaVG0T/BcBKPmPHrLmY\niuVXidltW7Jh9/RCVwb5+BvqlwlC470PEwhqoUatY/fPJ74srztrqJHvp1L29FT5\nsdmlJW8YwokCgYAUa3dMgp5C0knKp5RY1KSSU5E11w4zKZgwiWob4lq1dAPWtHpO\nGCo63yyBHImoUJVP75gUw4Cpc4EEudo5tlkIVuHV8nroGVKOhd9/Rb5K47Hke4kk\nBrn5a0Ues9qPDF65Fw1ryPDFSwHufjXAAO5SpZZJF51UGDgiNvDedbBgMQKBgHSk\nt7DjPhtW69234eCckD2fQS5ijBV1p2lMQmCygGM0dXiawvN02puOsCqDPoz+fxm2\nDwPY80cw0M0k9UeMnBxHt25JMDrDan/iTbxu++T/jlNrdebOXFlxlI5y3c7fULDS\nLZcNVzTXwhjlt7yp6d0NgzTyJw2ju9BiREfnTiRBAoGBAOPHrTOnPyjO+bVcCPTB\nWGLsbBd77mVPGIuL0XGrvbVYPE8yIcNbZcthd8VXL/38Ygy8SIZh2ZqsrU1b5WFa\nXUMLnGEODSS8x/GmW3i3KeirW5OxBNjfUzEF4XkJP8m41iTdsQEXQf9DdUY7X+CB\nVL5h7N0VstYhGgycuPpcIUQa\n-----END PRIVATE KEY-----\n"
|
||||||
|
}
|
||||||
9
tests/data/publickey_openssl.pem
Normal file
9
tests/data/publickey_openssl.pem
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4ej0p7bQ7L/r4rVGUz9R
|
||||||
|
N4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7
|
||||||
|
K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCa
|
||||||
|
kXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7
|
||||||
|
hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7q
|
||||||
|
iouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5O
|
||||||
|
YQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
@@ -220,6 +220,33 @@ class TestAppAssertionCredentials(unittest.TestCase):
|
|||||||
self.assertEqual('a_token_456', credentials.access_token)
|
self.assertEqual('a_token_456', credentials.access_token)
|
||||||
self.assertEqual(scope, credentials.scope)
|
self.assertEqual(scope, credentials.scope)
|
||||||
|
|
||||||
|
def test_create_scoped_required_without_scopes(self):
|
||||||
|
credentials = AppAssertionCredentials([])
|
||||||
|
self.assertTrue(credentials.create_scoped_required())
|
||||||
|
|
||||||
|
def test_create_scoped_required_with_scopes(self):
|
||||||
|
credentials = AppAssertionCredentials(['dummy_scope'])
|
||||||
|
self.assertFalse(credentials.create_scoped_required())
|
||||||
|
|
||||||
|
def test_create_scoped(self):
|
||||||
|
credentials = AppAssertionCredentials([])
|
||||||
|
new_credentials = credentials.create_scoped(['dummy_scope'])
|
||||||
|
self.assertNotEqual(credentials, new_credentials)
|
||||||
|
self.assertTrue(isinstance(new_credentials, AppAssertionCredentials))
|
||||||
|
self.assertEqual('dummy_scope', new_credentials.scope)
|
||||||
|
|
||||||
|
def test_get_access_token(self):
|
||||||
|
app_identity_stub = self.AppIdentityStubImpl()
|
||||||
|
apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
|
||||||
|
apiproxy_stub_map.apiproxy.RegisterStub("app_identity_service",
|
||||||
|
app_identity_stub)
|
||||||
|
apiproxy_stub_map.apiproxy.RegisterStub(
|
||||||
|
'memcache', memcache_stub.MemcacheServiceStub())
|
||||||
|
|
||||||
|
credentials = AppAssertionCredentials(['dummy_scope'])
|
||||||
|
token = credentials.get_access_token()
|
||||||
|
self.assertEqual('a_token_123', token)
|
||||||
|
|
||||||
|
|
||||||
class TestFlowModel(db.Model):
|
class TestFlowModel(db.Model):
|
||||||
flow = FlowProperty()
|
flow = FlowProperty()
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ Unit tests for oauth2client.gce.
|
|||||||
|
|
||||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||||
|
|
||||||
import unittest
|
import httplib2
|
||||||
import mox
|
import mox
|
||||||
|
import unittest
|
||||||
|
|
||||||
from oauth2client.client import AccessTokenRefreshError
|
from oauth2client.client import AccessTokenRefreshError
|
||||||
from oauth2client.client import Credentials
|
from oauth2client.client import Credentials
|
||||||
@@ -55,7 +56,6 @@ class AssertionCredentialsTests(unittest.TestCase):
|
|||||||
m.UnsetStubs()
|
m.UnsetStubs()
|
||||||
m.VerifyAll()
|
m.VerifyAll()
|
||||||
|
|
||||||
|
|
||||||
def test_fail_refresh(self):
|
def test_fail_refresh(self):
|
||||||
m = mox.Mox()
|
m = mox.Mox()
|
||||||
|
|
||||||
@@ -90,3 +90,44 @@ class AssertionCredentialsTests(unittest.TestCase):
|
|||||||
c2 = Credentials.new_from_json(json)
|
c2 = Credentials.new_from_json(json)
|
||||||
|
|
||||||
self.assertEqual(c.access_token, c2.access_token)
|
self.assertEqual(c.access_token, c2.access_token)
|
||||||
|
|
||||||
|
def test_create_scoped_required_without_scopes(self):
|
||||||
|
credentials = AppAssertionCredentials([])
|
||||||
|
self.assertTrue(credentials.create_scoped_required())
|
||||||
|
|
||||||
|
def test_create_scoped_required_with_scopes(self):
|
||||||
|
credentials = AppAssertionCredentials(['dummy_scope'])
|
||||||
|
self.assertFalse(credentials.create_scoped_required())
|
||||||
|
|
||||||
|
def test_create_scoped(self):
|
||||||
|
credentials = AppAssertionCredentials([])
|
||||||
|
new_credentials = credentials.create_scoped(['dummy_scope'])
|
||||||
|
self.assertNotEqual(credentials, new_credentials)
|
||||||
|
self.assertTrue(isinstance(new_credentials, AppAssertionCredentials))
|
||||||
|
self.assertEqual('dummy_scope', new_credentials.scope)
|
||||||
|
|
||||||
|
def test_get_access_token(self):
|
||||||
|
m = mox.Mox()
|
||||||
|
|
||||||
|
httplib2_response = m.CreateMock(object)
|
||||||
|
httplib2_response.status = 200
|
||||||
|
|
||||||
|
httplib2_request = m.CreateMock(object)
|
||||||
|
httplib2_request.__call__(
|
||||||
|
('http://metadata.google.internal/0.1/meta-data/service-accounts/'
|
||||||
|
'default/acquire?scope=dummy_scope'
|
||||||
|
)).AndReturn((httplib2_response, '{"accessToken": "this-is-a-token"}'))
|
||||||
|
|
||||||
|
m.ReplayAll()
|
||||||
|
|
||||||
|
credentials = AppAssertionCredentials(['dummy_scope'])
|
||||||
|
|
||||||
|
http = httplib2.Http()
|
||||||
|
http.request = httplib2_request
|
||||||
|
|
||||||
|
self.assertEquals('this-is-a-token',
|
||||||
|
credentials.get_access_token(http=http))
|
||||||
|
|
||||||
|
m.UnsetStubs()
|
||||||
|
m.VerifyAll()
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
|
import mox
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
@@ -37,23 +39,36 @@ from oauth2client.client import AccessTokenCredentials
|
|||||||
from oauth2client.client import AccessTokenCredentialsError
|
from oauth2client.client import AccessTokenCredentialsError
|
||||||
from oauth2client.client import AccessTokenRefreshError
|
from oauth2client.client import AccessTokenRefreshError
|
||||||
from oauth2client.client import AssertionCredentials
|
from oauth2client.client import AssertionCredentials
|
||||||
|
from oauth2client.client import AUTHORIZED_USER
|
||||||
from oauth2client.client import Credentials
|
from oauth2client.client import Credentials
|
||||||
|
from oauth2client.client import DefaultCredentialsError
|
||||||
from oauth2client.client import FlowExchangeError
|
from oauth2client.client import FlowExchangeError
|
||||||
|
from oauth2client.client import GoogleCredentials
|
||||||
|
from oauth2client.client import GOOGLE_CREDENTIALS_DEFAULT
|
||||||
from oauth2client.client import MemoryCache
|
from oauth2client.client import MemoryCache
|
||||||
from oauth2client.client import NonAsciiHeaderError
|
from oauth2client.client import NonAsciiHeaderError
|
||||||
from oauth2client.client import OAuth2Credentials
|
from oauth2client.client import OAuth2Credentials
|
||||||
from oauth2client.client import OAuth2WebServerFlow
|
from oauth2client.client import OAuth2WebServerFlow
|
||||||
from oauth2client.client import OOB_CALLBACK_URN
|
from oauth2client.client import OOB_CALLBACK_URN
|
||||||
from oauth2client.client import REFRESH_STATUS_CODES
|
from oauth2client.client import REFRESH_STATUS_CODES
|
||||||
|
from oauth2client.client import SERVICE_ACCOUNT
|
||||||
from oauth2client.client import Storage
|
from oauth2client.client import Storage
|
||||||
from oauth2client.client import TokenRevokeError
|
from oauth2client.client import TokenRevokeError
|
||||||
from oauth2client.client import VerifyJwtTokenError
|
from oauth2client.client import VerifyJwtTokenError
|
||||||
|
from oauth2client.client import _env_name
|
||||||
from oauth2client.client import _extract_id_token
|
from oauth2client.client import _extract_id_token
|
||||||
|
from oauth2client.client import _get_default_credential_from_file
|
||||||
|
from oauth2client.client import _get_environment
|
||||||
|
from oauth2client.client import _get_environment_variable_file
|
||||||
|
from oauth2client.client import _get_well_known_file
|
||||||
|
from oauth2client.client import _raise_exception_for_missing_fields
|
||||||
|
from oauth2client.client import _raise_exception_for_reading_json
|
||||||
from oauth2client.client import _update_query_params
|
from oauth2client.client import _update_query_params
|
||||||
from oauth2client.client import credentials_from_clientsecrets_and_code
|
from oauth2client.client import credentials_from_clientsecrets_and_code
|
||||||
from oauth2client.client import credentials_from_code
|
from oauth2client.client import credentials_from_code
|
||||||
from oauth2client.client import flow_from_clientsecrets
|
from oauth2client.client import flow_from_clientsecrets
|
||||||
from oauth2client.clientsecrets import _loadfile
|
from oauth2client.clientsecrets import _loadfile
|
||||||
|
from oauth2client.service_account import _ServiceAccountCredentials
|
||||||
|
|
||||||
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
|
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
|
||||||
|
|
||||||
@@ -107,6 +122,328 @@ class CredentialsTests(unittest.TestCase):
|
|||||||
restored = Credentials.new_from_json(json)
|
restored = Credentials.new_from_json(json)
|
||||||
|
|
||||||
|
|
||||||
|
class MockResponse(object):
|
||||||
|
"""Mock the response of urllib2.urlopen() call."""
|
||||||
|
|
||||||
|
def __init__(self, headers):
|
||||||
|
self._headers = headers
|
||||||
|
|
||||||
|
def info(self):
|
||||||
|
class Info:
|
||||||
|
def __init__(self, headers):
|
||||||
|
self.headers = headers
|
||||||
|
|
||||||
|
return Info(self._headers)
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleCredentialsTests(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.env_server_software = os.environ.get('SERVER_SOFTWARE', None)
|
||||||
|
self.env_google_credentials_default = (
|
||||||
|
os.environ.get(GOOGLE_CREDENTIALS_DEFAULT, None))
|
||||||
|
self.env_appdata = os.environ.get('APPDATA', None)
|
||||||
|
self.os_name = os.name
|
||||||
|
from oauth2client import client
|
||||||
|
setattr(client, '_env_name', None)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.reset_env('SERVER_SOFTWARE', self.env_server_software)
|
||||||
|
self.reset_env(GOOGLE_CREDENTIALS_DEFAULT,
|
||||||
|
self.env_google_credentials_default)
|
||||||
|
self.reset_env('APPDATA', self.env_appdata)
|
||||||
|
os.name = self.os_name
|
||||||
|
|
||||||
|
def reset_env(self, env, value):
|
||||||
|
"""Set the environment variable 'env' to 'value'."""
|
||||||
|
if value is not None:
|
||||||
|
os.environ[env] = value
|
||||||
|
else:
|
||||||
|
os.environ.pop(env, '')
|
||||||
|
|
||||||
|
def validate_service_account_credentials(self, credentials):
|
||||||
|
self.assertTrue(isinstance(credentials, _ServiceAccountCredentials))
|
||||||
|
self.assertEqual('123', credentials._service_account_id)
|
||||||
|
self.assertEqual('dummy@google.com', credentials._service_account_email)
|
||||||
|
self.assertEqual('ABCDEF', credentials._private_key_id)
|
||||||
|
self.assertEqual('', credentials._scopes)
|
||||||
|
|
||||||
|
def validate_google_credentials(self, credentials):
|
||||||
|
self.assertTrue(isinstance(credentials, GoogleCredentials))
|
||||||
|
self.assertEqual(None, credentials.access_token)
|
||||||
|
self.assertEqual('123', credentials.client_id)
|
||||||
|
self.assertEqual('secret', credentials.client_secret)
|
||||||
|
self.assertEqual('alabalaportocala', credentials.refresh_token)
|
||||||
|
self.assertEqual(None, credentials.token_expiry)
|
||||||
|
self.assertEqual(GOOGLE_TOKEN_URI, credentials.token_uri)
|
||||||
|
self.assertEqual('Python client library', credentials.user_agent)
|
||||||
|
|
||||||
|
def get_a_google_credentials_object(self):
|
||||||
|
return GoogleCredentials(None, None, None, None, None, None, None, None)
|
||||||
|
|
||||||
|
def test_create_scoped_required(self):
|
||||||
|
self.assertFalse(
|
||||||
|
self.get_a_google_credentials_object().create_scoped_required())
|
||||||
|
|
||||||
|
def test_create_scoped(self):
|
||||||
|
credentials = self.get_a_google_credentials_object()
|
||||||
|
self.assertEqual(credentials, credentials.create_scoped(None))
|
||||||
|
self.assertEqual(credentials,
|
||||||
|
credentials.create_scoped(['dummy_scope']))
|
||||||
|
|
||||||
|
def test_get_environment_gae_production(self):
|
||||||
|
os.environ['SERVER_SOFTWARE'] = 'Google App Engine/XYZ'
|
||||||
|
self.assertEqual('GAE_PRODUCTION', _get_environment())
|
||||||
|
|
||||||
|
def test_get_environment_gae_local(self):
|
||||||
|
os.environ['SERVER_SOFTWARE'] = 'Development/XYZ'
|
||||||
|
self.assertEqual('GAE_LOCAL', _get_environment())
|
||||||
|
|
||||||
|
def test_get_environment_gce_production(self):
|
||||||
|
os.environ['SERVER_SOFTWARE'] = ''
|
||||||
|
mockResponse = MockResponse(['Metadata-Flavor: Google\r\n'])
|
||||||
|
|
||||||
|
m = mox.Mox()
|
||||||
|
|
||||||
|
urllib2_urlopen = m.CreateMock(object)
|
||||||
|
urllib2_urlopen.__call__(('http://metadata.google.internal'
|
||||||
|
)).AndReturn(mockResponse)
|
||||||
|
|
||||||
|
m.ReplayAll()
|
||||||
|
|
||||||
|
self.assertEqual('GCE_PRODUCTION', _get_environment(urllib2_urlopen))
|
||||||
|
|
||||||
|
m.UnsetStubs()
|
||||||
|
m.VerifyAll()
|
||||||
|
|
||||||
|
def test_get_environment_unknown(self):
|
||||||
|
os.environ['SERVER_SOFTWARE'] = ''
|
||||||
|
mockResponse = MockResponse([])
|
||||||
|
|
||||||
|
m = mox.Mox()
|
||||||
|
|
||||||
|
urllib2_urlopen = m.CreateMock(object)
|
||||||
|
urllib2_urlopen.__call__(('http://metadata.google.internal'
|
||||||
|
)).AndReturn(mockResponse)
|
||||||
|
|
||||||
|
m.ReplayAll()
|
||||||
|
|
||||||
|
self.assertEqual('UNKNOWN', _get_environment(urllib2_urlopen))
|
||||||
|
|
||||||
|
m.UnsetStubs()
|
||||||
|
m.VerifyAll()
|
||||||
|
|
||||||
|
def test_get_environment_variable_file(self):
|
||||||
|
environment_variable_file = datafile(
|
||||||
|
os.path.join('gcloud', 'credentials_default.json'))
|
||||||
|
os.environ[GOOGLE_CREDENTIALS_DEFAULT] = environment_variable_file
|
||||||
|
self.assertEqual(environment_variable_file,
|
||||||
|
_get_environment_variable_file())
|
||||||
|
|
||||||
|
def test_get_environment_variable_file_error(self):
|
||||||
|
nonexistent_file = datafile('nonexistent')
|
||||||
|
os.environ[GOOGLE_CREDENTIALS_DEFAULT] = nonexistent_file
|
||||||
|
# we can't use self.assertRaisesRegexp() because it is only in Python 2.7+
|
||||||
|
try:
|
||||||
|
_get_environment_variable_file()
|
||||||
|
self.fail(nonexistent_file + ' should not exist.')
|
||||||
|
except DefaultCredentialsError as error:
|
||||||
|
self.assertEqual('File ' + nonexistent_file +
|
||||||
|
' (pointed by ' + GOOGLE_CREDENTIALS_DEFAULT +
|
||||||
|
' environment variable) does not exist!',
|
||||||
|
str(error))
|
||||||
|
|
||||||
|
def test_get_well_known_file_on_windows(self):
|
||||||
|
well_known_file = datafile(
|
||||||
|
os.path.join('gcloud', 'credentials_default.json'))
|
||||||
|
os.name = 'nt'
|
||||||
|
os.environ['APPDATA'] = DATA_DIR
|
||||||
|
self.assertEqual(well_known_file, _get_well_known_file())
|
||||||
|
|
||||||
|
def test_get_well_known_file_on_windows_no_file(self):
|
||||||
|
os.name = 'nt'
|
||||||
|
os.environ['APPDATA'] = os.path.join(DATA_DIR, 'nonexistentpath')
|
||||||
|
self.assertEqual(None, _get_well_known_file())
|
||||||
|
|
||||||
|
def test_get_default_credential_from_file_service_account(self):
|
||||||
|
credentials_file = datafile(
|
||||||
|
os.path.join('gcloud', 'credentials_default.json'))
|
||||||
|
credentials = _get_default_credential_from_file(credentials_file)
|
||||||
|
self.validate_service_account_credentials(credentials)
|
||||||
|
|
||||||
|
def test_get_default_credential_from_file_authorized_user(self):
|
||||||
|
credentials_file = datafile(
|
||||||
|
os.path.join('gcloud', 'credentials_default_authorized_user.json'))
|
||||||
|
credentials = _get_default_credential_from_file(credentials_file)
|
||||||
|
self.validate_google_credentials(credentials)
|
||||||
|
|
||||||
|
def test_get_default_credential_from_malformed_file_1(self):
|
||||||
|
credentials_file = datafile(
|
||||||
|
os.path.join('gcloud', 'credentials_default_malformed_1.json'))
|
||||||
|
# we can't use self.assertRaisesRegexp() because it is only in Python 2.7+
|
||||||
|
try:
|
||||||
|
_get_default_credential_from_file(credentials_file)
|
||||||
|
self.fail('An exception was expected!')
|
||||||
|
except DefaultCredentialsError as error:
|
||||||
|
self.assertEqual("'type' field should be defined "
|
||||||
|
"(and have one of the '" + AUTHORIZED_USER +
|
||||||
|
"' or '" + SERVICE_ACCOUNT + "' values)",
|
||||||
|
str(error))
|
||||||
|
|
||||||
|
def test_get_default_credential_from_malformed_file_2(self):
|
||||||
|
credentials_file = datafile(
|
||||||
|
os.path.join('gcloud', 'credentials_default_malformed_2.json'))
|
||||||
|
# we can't use self.assertRaisesRegexp() because it is only in Python 2.7+
|
||||||
|
try:
|
||||||
|
_get_default_credential_from_file(credentials_file)
|
||||||
|
self.fail('An exception was expected!')
|
||||||
|
except DefaultCredentialsError as error:
|
||||||
|
self.assertEqual('The following field(s): '
|
||||||
|
'private_key_id must be defined.',
|
||||||
|
str(error))
|
||||||
|
|
||||||
|
def test_get_default_credential_from_malformed_file_3(self):
|
||||||
|
credentials_file = datafile(
|
||||||
|
os.path.join('gcloud', 'credentials_default_malformed_3.json'))
|
||||||
|
self.assertRaises(ValueError, _get_default_credential_from_file,
|
||||||
|
credentials_file)
|
||||||
|
|
||||||
|
def test_raise_exception_for_missing_fields(self):
|
||||||
|
missing_fields = ['first', 'second', 'third']
|
||||||
|
# we can't use self.assertRaisesRegexp() because it is only in Python 2.7+
|
||||||
|
try:
|
||||||
|
_raise_exception_for_missing_fields(missing_fields)
|
||||||
|
self.fail('An exception was expected!')
|
||||||
|
except DefaultCredentialsError as error:
|
||||||
|
self.assertEqual('The following field(s): ' +
|
||||||
|
', '.join(missing_fields) + ' must be defined.',
|
||||||
|
str(error))
|
||||||
|
|
||||||
|
def test_raise_exception_for_reading_json(self):
|
||||||
|
credential_file = 'any_file'
|
||||||
|
extra_help = ' be good'
|
||||||
|
error = DefaultCredentialsError('stuff happens')
|
||||||
|
# we can't use self.assertRaisesRegexp() because it is only in Python 2.7+
|
||||||
|
try:
|
||||||
|
_raise_exception_for_reading_json(credential_file, extra_help, error)
|
||||||
|
self.fail('An exception was expected!')
|
||||||
|
except DefaultCredentialsError as ex:
|
||||||
|
self.assertEqual('An error was encountered while reading '
|
||||||
|
'json file: '+ credential_file +
|
||||||
|
extra_help + ': ' + str(error),
|
||||||
|
str(ex))
|
||||||
|
|
||||||
|
def test_get_default_from_environment_variable_service_account(self):
|
||||||
|
os.environ['SERVER_SOFTWARE'] = ''
|
||||||
|
environment_variable_file = datafile(
|
||||||
|
os.path.join('gcloud', 'credentials_default.json'))
|
||||||
|
os.environ[GOOGLE_CREDENTIALS_DEFAULT] = environment_variable_file
|
||||||
|
self.validate_service_account_credentials(GoogleCredentials.get_default())
|
||||||
|
|
||||||
|
def test_env_name(self):
|
||||||
|
from oauth2client import client
|
||||||
|
self.assertEqual(None, getattr(client, '_env_name'))
|
||||||
|
self.test_get_default_from_environment_variable_service_account()
|
||||||
|
self.assertEqual('UNKNOWN', getattr(client, '_env_name'))
|
||||||
|
|
||||||
|
def test_get_default_from_environment_variable_authorized_user(self):
|
||||||
|
os.environ['SERVER_SOFTWARE'] = ''
|
||||||
|
environment_variable_file = datafile(
|
||||||
|
os.path.join('gcloud', 'credentials_default_authorized_user.json'))
|
||||||
|
os.environ[GOOGLE_CREDENTIALS_DEFAULT] = environment_variable_file
|
||||||
|
self.validate_google_credentials(GoogleCredentials.get_default())
|
||||||
|
|
||||||
|
def test_get_default_from_environment_variable_malformed_file(self):
|
||||||
|
os.environ['SERVER_SOFTWARE'] = ''
|
||||||
|
environment_variable_file = datafile(
|
||||||
|
os.path.join('gcloud', 'credentials_default_malformed_3.json'))
|
||||||
|
os.environ[GOOGLE_CREDENTIALS_DEFAULT] = environment_variable_file
|
||||||
|
# we can't use self.assertRaisesRegexp() because it is only in Python 2.7+
|
||||||
|
try:
|
||||||
|
GoogleCredentials.get_default()
|
||||||
|
self.fail('An exception was expected!')
|
||||||
|
except DefaultCredentialsError as error:
|
||||||
|
self.assertTrue(str(error).startswith(
|
||||||
|
'An error was encountered while reading json file: ' +
|
||||||
|
environment_variable_file + ' (pointed to by ' +
|
||||||
|
GOOGLE_CREDENTIALS_DEFAULT + ' environment variable):'))
|
||||||
|
|
||||||
|
def test_get_default_environment_not_set_up(self):
|
||||||
|
# It is normal for this test to fail if run inside
|
||||||
|
# a Google Compute Engine VM or after 'gcloud auth login' command
|
||||||
|
# has been executed on a non Windows machine.
|
||||||
|
os.environ['SERVER_SOFTWARE'] = ''
|
||||||
|
os.environ[GOOGLE_CREDENTIALS_DEFAULT] = ''
|
||||||
|
os.environ['APPDATA'] = ''
|
||||||
|
# we can't use self.assertRaisesRegexp() because it is only in Python 2.7+
|
||||||
|
try:
|
||||||
|
GoogleCredentials.get_default()
|
||||||
|
self.fail('An exception was expected!')
|
||||||
|
except DefaultCredentialsError as error:
|
||||||
|
self.assertEqual("The Default Credentials are not available. They are "
|
||||||
|
"available if running in Google App Engine or Google "
|
||||||
|
"Compute Engine. They are also available if using the "
|
||||||
|
"Google Cloud SDK and running 'gcloud auth login'. "
|
||||||
|
"Otherwise, the environment variable " +
|
||||||
|
GOOGLE_CREDENTIALS_DEFAULT + " must be defined pointing "
|
||||||
|
"to a file defining the credentials. See "
|
||||||
|
"https://developers.google.com/accounts/docs/default-"
|
||||||
|
"credentials for details.",
|
||||||
|
str(error))
|
||||||
|
|
||||||
|
def test_from_stream_service_account(self):
|
||||||
|
credentials_file = datafile(
|
||||||
|
os.path.join('gcloud', 'credentials_default.json'))
|
||||||
|
credentials = (
|
||||||
|
self.get_a_google_credentials_object().from_stream(credentials_file))
|
||||||
|
self.validate_service_account_credentials(credentials)
|
||||||
|
|
||||||
|
def test_from_stream_authorized_user(self):
|
||||||
|
credentials_file = datafile(
|
||||||
|
os.path.join('gcloud', 'credentials_default_authorized_user.json'))
|
||||||
|
credentials = (
|
||||||
|
self.get_a_google_credentials_object().from_stream(credentials_file))
|
||||||
|
self.validate_google_credentials(credentials)
|
||||||
|
|
||||||
|
def test_from_stream_malformed_file_1(self):
|
||||||
|
credentials_file = datafile(
|
||||||
|
os.path.join('gcloud', 'credentials_default_malformed_1.json'))
|
||||||
|
# we can't use self.assertRaisesRegexp() because it is only in Python 2.7+
|
||||||
|
try:
|
||||||
|
self.get_a_google_credentials_object().from_stream(credentials_file)
|
||||||
|
self.fail('An exception was expected!')
|
||||||
|
except DefaultCredentialsError as error:
|
||||||
|
self.assertEqual("An error was encountered while reading json file: " +
|
||||||
|
credentials_file +
|
||||||
|
" (provided as parameter to the from_stream() method): "
|
||||||
|
"'type' field should be defined (and have one of the '" +
|
||||||
|
AUTHORIZED_USER + "' or '" + SERVICE_ACCOUNT +
|
||||||
|
"' values)",
|
||||||
|
str(error))
|
||||||
|
|
||||||
|
def test_from_stream_malformed_file_2(self):
|
||||||
|
credentials_file = datafile(
|
||||||
|
os.path.join('gcloud', 'credentials_default_malformed_2.json'))
|
||||||
|
# we can't use self.assertRaisesRegexp() because it is only in Python 2.7+
|
||||||
|
try:
|
||||||
|
self.get_a_google_credentials_object().from_stream(credentials_file)
|
||||||
|
self.fail('An exception was expected!')
|
||||||
|
except DefaultCredentialsError as error:
|
||||||
|
self.assertEqual('An error was encountered while reading json file: ' +
|
||||||
|
credentials_file +
|
||||||
|
' (provided as parameter to the from_stream() method): '
|
||||||
|
'The following field(s): private_key_id must be '
|
||||||
|
'defined.',
|
||||||
|
str(error))
|
||||||
|
|
||||||
|
def test_from_stream_malformed_file_3(self):
|
||||||
|
credentials_file = datafile(
|
||||||
|
os.path.join('gcloud', 'credentials_default_malformed_3.json'))
|
||||||
|
self.assertRaises(
|
||||||
|
DefaultCredentialsError,
|
||||||
|
self.get_a_google_credentials_object().from_stream, credentials_file)
|
||||||
|
|
||||||
|
|
||||||
class DummyDeleteStorage(Storage):
|
class DummyDeleteStorage(Storage):
|
||||||
delete_called = False
|
delete_called = False
|
||||||
|
|
||||||
@@ -245,6 +582,32 @@ class BasicCredentialsTests(unittest.TestCase):
|
|||||||
instance = OAuth2Credentials.from_json(self.credentials.to_json())
|
instance = OAuth2Credentials.from_json(self.credentials.to_json())
|
||||||
self.assertEqual('foobar', instance.token_response)
|
self.assertEqual('foobar', instance.token_response)
|
||||||
|
|
||||||
|
def test_get_access_token(self):
|
||||||
|
token_response_first = {'access_token': 'first_token', 'expires_in': 1}
|
||||||
|
token_response_second = {'access_token': 'second_token', 'expires_in': 1}
|
||||||
|
http = HttpMockSequence([
|
||||||
|
({'status': '200'}, simplejson.dumps(token_response_first)),
|
||||||
|
({'status': '200'}, simplejson.dumps(token_response_second)),
|
||||||
|
])
|
||||||
|
|
||||||
|
self.assertEqual('first_token',
|
||||||
|
self.credentials.get_access_token(http=http))
|
||||||
|
self.assertFalse(self.credentials.access_token_expired)
|
||||||
|
self.assertEqual(token_response_first, self.credentials.token_response)
|
||||||
|
|
||||||
|
self.assertEqual('first_token',
|
||||||
|
self.credentials.get_access_token(http=http))
|
||||||
|
self.assertFalse(self.credentials.access_token_expired)
|
||||||
|
self.assertEqual(token_response_first, self.credentials.token_response)
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
self.assertTrue(self.credentials.access_token_expired)
|
||||||
|
|
||||||
|
self.assertEqual('second_token',
|
||||||
|
self.credentials.get_access_token(http=http))
|
||||||
|
self.assertFalse(self.credentials.access_token_expired)
|
||||||
|
self.assertEqual(token_response_second, self.credentials.token_response)
|
||||||
|
|
||||||
|
|
||||||
class AccessTokenCredentialsTests(unittest.TestCase):
|
class AccessTokenCredentialsTests(unittest.TestCase):
|
||||||
|
|
||||||
|
|||||||
120
tests/test_service_account.py
Normal file
120
tests/test_service_account.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/python2.4
|
||||||
|
#
|
||||||
|
# Copyright 2014 Google Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
"""Oauth2client tests.
|
||||||
|
|
||||||
|
Unit tests for service account credentials implemented using RSA.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import rsa
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from http_mock import HttpMockSequence
|
||||||
|
from oauth2client.anyjson import simplejson
|
||||||
|
from oauth2client.service_account import _ServiceAccountCredentials
|
||||||
|
|
||||||
|
|
||||||
|
def datafile(filename):
|
||||||
|
# TODO(orestica): Refactor this using pkgutil.get_data
|
||||||
|
f = open(os.path.join(os.path.dirname(__file__), 'data', filename), 'r')
|
||||||
|
data = f.read()
|
||||||
|
f.close()
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceAccountCredentialsTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.service_account_id = '123'
|
||||||
|
self.service_account_email = 'dummy@google.com'
|
||||||
|
self.private_key_id = 'ABCDEF'
|
||||||
|
self.private_key = datafile('pem_from_pkcs12.pem')
|
||||||
|
self.scopes = ['dummy_scope']
|
||||||
|
self.credentials = _ServiceAccountCredentials(self.service_account_id,
|
||||||
|
self.service_account_email,
|
||||||
|
self.private_key_id,
|
||||||
|
self.private_key,
|
||||||
|
[])
|
||||||
|
|
||||||
|
def test_sign_blob(self):
|
||||||
|
private_key_id, signature = self.credentials.sign_blob('Google')
|
||||||
|
self.assertEqual( self.private_key_id, private_key_id)
|
||||||
|
|
||||||
|
pub_key = rsa.PublicKey.load_pkcs1_openssl_pem(
|
||||||
|
datafile('publickey_openssl.pem'))
|
||||||
|
|
||||||
|
self.assertTrue(rsa.pkcs1.verify('Google', signature, pub_key))
|
||||||
|
|
||||||
|
try:
|
||||||
|
rsa.pkcs1.verify('Orest', signature, pub_key)
|
||||||
|
self.fail('Verification should have failed!')
|
||||||
|
except rsa.pkcs1.VerificationError:
|
||||||
|
pass # Expected
|
||||||
|
|
||||||
|
try:
|
||||||
|
rsa.pkcs1.verify('Google', 'bad signature', pub_key)
|
||||||
|
self.fail('Verification should have failed!')
|
||||||
|
except rsa.pkcs1.VerificationError:
|
||||||
|
pass # Expected
|
||||||
|
|
||||||
|
def test_service_account_email(self):
|
||||||
|
self.assertEqual(self.service_account_email,
|
||||||
|
self.credentials.service_account_email)
|
||||||
|
|
||||||
|
def test_create_scoped_required_without_scopes(self):
|
||||||
|
self.assertTrue(self.credentials.create_scoped_required())
|
||||||
|
|
||||||
|
def test_create_scoped_required_with_scopes(self):
|
||||||
|
self.credentials = _ServiceAccountCredentials(self.service_account_id,
|
||||||
|
self.service_account_email,
|
||||||
|
self.private_key_id,
|
||||||
|
self.private_key,
|
||||||
|
self.scopes)
|
||||||
|
self.assertFalse(self.credentials.create_scoped_required())
|
||||||
|
|
||||||
|
def test_create_scoped(self):
|
||||||
|
new_credentials = self.credentials.create_scoped(self.scopes)
|
||||||
|
self.assertNotEqual(self.credentials, new_credentials)
|
||||||
|
self.assertTrue(isinstance(new_credentials, _ServiceAccountCredentials))
|
||||||
|
self.assertEqual('dummy_scope', new_credentials._scopes)
|
||||||
|
|
||||||
|
def test_access_token(self):
|
||||||
|
token_response_first = {'access_token': 'first_token', 'expires_in': 1}
|
||||||
|
token_response_second = {'access_token': 'second_token', 'expires_in': 1}
|
||||||
|
http = HttpMockSequence([
|
||||||
|
({'status': '200'}, simplejson.dumps(token_response_first)),
|
||||||
|
({'status': '200'}, simplejson.dumps(token_response_second)),
|
||||||
|
])
|
||||||
|
|
||||||
|
self.assertEqual('first_token',
|
||||||
|
self.credentials.get_access_token(http=http))
|
||||||
|
self.assertFalse(self.credentials.access_token_expired)
|
||||||
|
self.assertEqual(token_response_first, self.credentials.token_response)
|
||||||
|
|
||||||
|
self.assertEqual('first_token',
|
||||||
|
self.credentials.get_access_token(http=http))
|
||||||
|
self.assertFalse(self.credentials.access_token_expired)
|
||||||
|
self.assertEqual(token_response_first, self.credentials.token_response)
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
self.assertTrue(self.credentials.access_token_expired)
|
||||||
|
|
||||||
|
self.assertEqual('second_token',
|
||||||
|
self.credentials.get_access_token(http=http))
|
||||||
|
self.assertFalse(self.credentials.access_token_expired)
|
||||||
|
self.assertEqual(token_response_second, self.credentials.token_response)
|
||||||
Reference in New Issue
Block a user