Skip to content

Commit 569f4e0

Browse files
author
Orest Bolohan
committed
Add support for Google Default Credentials.
1 parent e48d951 commit 569f4e0

18 files changed

+1120
-6
lines changed

oauth2client/appengine.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ def __init__(self, scope, **kwargs):
164164
unspecified, the default service account for the app is used.
165165
"""
166166
self.scope = util.scopes_to_string(scope)
167+
self._kwargs = kwargs
167168
self.service_account_id = kwargs.get('service_account_id', None)
168169

169170
# Assertion type is no longer used, but still in the parent class signature.
@@ -196,6 +197,12 @@ def _refresh(self, http_request):
196197
raise AccessTokenRefreshError(str(e))
197198
self.access_token = token
198199

200+
def create_scoped_required(self):
201+
return not self.scope
202+
203+
def create_scoped(self, scopes):
204+
return AppAssertionCredentials(scopes, **self._kwargs)
205+
199206

200207
class FlowProperty(db.Property):
201208
"""App Engine datastore Property for Flow.

oauth2client/client.py

Lines changed: 326 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@
6666
# Google Data client libraries may need to set this to [401, 403].
6767
REFRESH_STATUS_CODES = [401]
6868

69+
# The value representing user credentials.
70+
AUTHORIZED_USER = 'authorized_user'
71+
72+
# The value representing service account credentials.
73+
SERVICE_ACCOUNT = 'service_account'
74+
75+
# The environment variable pointing the file with local Default Credentials.
76+
GOOGLE_CREDENTIALS_DEFAULT = 'GOOGLE_CREDENTIALS_DEFAULT'
6977

7078
class Error(Exception):
7179
"""Base error for this module."""
@@ -99,6 +107,10 @@ class NonAsciiHeaderError(Error):
99107
"""Header names and values must be ASCII strings."""
100108

101109

110+
class DefaultCredentialsError(Error):
111+
"""Error retrieving the Default Credentials."""
112+
113+
102114
def _abstract():
103115
raise NotImplementedError('You need to override this function')
104116

@@ -126,7 +138,7 @@ class Credentials(object):
126138
an HTTP transport.
127139
128140
Subclasses must also specify a classmethod named 'from_json' that takes a JSON
129-
string as input and returns an instaniated Credentials object.
141+
string as input and returns an instantiated Credentials object.
130142
"""
131143

132144
NON_SERIALIZED_MEMBERS = ['store']
@@ -375,7 +387,7 @@ def _update_query_params(uri, params):
375387
The same URI but with the new query parameters added.
376388
"""
377389
parts = list(urlparse.urlparse(uri))
378-
query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part
390+
query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part
379391
query_params.update(params)
380392
parts[4] = urllib.urlencode(query_params)
381393
return urlparse.urlunparse(parts)
@@ -587,6 +599,20 @@ def access_token_expired(self):
587599
return True
588600
return False
589601

602+
def get_access_token(self, http=None):
603+
"""Return the access token.
604+
605+
If the token does not exist, get one.
606+
If the token expired, refresh it.
607+
"""
608+
if self.access_token and not self.access_token_expired:
609+
return self.access_token
610+
else:
611+
if not http:
612+
http = httplib2.Http()
613+
self.refresh(http)
614+
return self.access_token
615+
590616
def set_store(self, store):
591617
"""Set the Storage for the credential.
592618
@@ -820,7 +846,303 @@ def _revoke(self, http_request):
820846
self._do_revoke(http_request, self.access_token)
821847

822848

823-
class AssertionCredentials(OAuth2Credentials):
849+
_env_name = None
850+
851+
852+
def _get_environment(urllib2_urlopen=None):
853+
"""Detect the environment the code is being run on."""
854+
855+
global _env_name
856+
857+
if _env_name:
858+
return _env_name
859+
860+
server_software = os.environ.get('SERVER_SOFTWARE', '')
861+
if server_software.startswith('Google App Engine/'):
862+
_env_name = 'GAE_PRODUCTION'
863+
elif server_software.startswith('Development/'):
864+
_env_name = 'GAE_LOCAL'
865+
else:
866+
import urllib2
867+
try:
868+
if urllib2_urlopen is None:
869+
urllib2_urlopen = urllib2.urlopen
870+
response = urllib2_urlopen('http://metadata.google.internal')
871+
if any('Metadata-Flavor: Google' in h for h in response.info().headers):
872+
_env_name = 'GCE_PRODUCTION'
873+
else:
874+
_env_name = 'UNKNOWN'
875+
except urllib2.URLError:
876+
_env_name = 'UNKNOWN'
877+
878+
return _env_name
879+
880+
881+
class GoogleCredentials(OAuth2Credentials):
882+
"""Default credentials for use in calling Google APIs.
883+
884+
The Default Credentials are being constructed as a function of the environment
885+
where the code is being run. More details can be found on this page:
886+
https://developers.google.com/accounts/docs/default-credentials
887+
888+
Here is an example of how to use the Default Credentials for a service that
889+
requires authentication:
890+
891+
<code>
892+
from googleapiclient.discovery import build
893+
from oauth2client.client import GoogleCredentials
894+
895+
PROJECT = 'bamboo-machine-422' # replace this with one of your projects
896+
ZONE = 'us-central1-a' # replace this with the zone you care about
897+
898+
service = build('compute', 'v1', credentials=GoogleCredentials.get_default())
899+
900+
request = service.instances().list(project=PROJECT, zone=ZONE)
901+
response = request.execute()
902+
903+
print response
904+
</code>
905+
906+
A service that does not require authentication does not need credentials
907+
to be passed in:
908+
909+
<code>
910+
from googleapiclient.discovery import build
911+
912+
service = build('discovery', 'v1')
913+
914+
request = service.apis().list()
915+
response = request.execute()
916+
917+
print response
918+
</code>
919+
"""
920+
921+
def __init__(self, access_token, client_id, client_secret, refresh_token,
922+
token_expiry, token_uri, user_agent,
923+
revoke_uri=GOOGLE_REVOKE_URI):
924+
"""Create an instance of GoogleCredentials.
925+
926+
This constructor is not usually called by the user, instead
927+
GoogleCredentials objects are instantiated by
928+
GoogleCredentials.from_stream() or GoogleCredentials.get_default().
929+
930+
Args:
931+
access_token: string, access token.
932+
client_id: string, client identifier.
933+
client_secret: string, client secret.
934+
refresh_token: string, refresh token.
935+
token_expiry: datetime, when the access_token expires.
936+
token_uri: string, URI of token endpoint.
937+
user_agent: string, The HTTP User-Agent to provide for this application.
938+
revoke_uri: string, URI for revoke endpoint.
939+
Defaults to GOOGLE_REVOKE_URI; a token can't be revoked if this is None.
940+
"""
941+
super(GoogleCredentials, self).__init__(
942+
access_token, client_id, client_secret, refresh_token, token_expiry,
943+
token_uri, user_agent, revoke_uri=revoke_uri)
944+
945+
def create_scoped_required(self):
946+
"""Whether this Credentials object is scopeless.
947+
948+
create_scoped(scopes) method needs to be called in order to create
949+
a Credentials object for API calls.
950+
"""
951+
return False
952+
953+
def create_scoped(self, scopes):
954+
"""Create a Credentials object for the given scopes.
955+
956+
The Credentials type is preserved.
957+
"""
958+
return self
959+
960+
@staticmethod
961+
def get_default():
962+
"""Get the Default Credentials for the current environment.
963+
964+
Exceptions:
965+
DefaultCredentialsError: raised when the credentials fail to be retrieved.
966+
"""
967+
968+
_env_name = _get_environment()
969+
970+
if _env_name in ('GAE_PRODUCTION', 'GAE_LOCAL'):
971+
# if we are running inside Google App Engine
972+
# there is no need to look for credentials in local files
973+
default_credential_filename = None
974+
well_known_file = None
975+
else:
976+
default_credential_filename = _get_environment_variable_file()
977+
well_known_file = _get_well_known_file()
978+
979+
if default_credential_filename:
980+
try:
981+
return _get_default_credential_from_file(default_credential_filename)
982+
except (DefaultCredentialsError, ValueError) as error:
983+
extra_help = (' (pointed to by ' + GOOGLE_CREDENTIALS_DEFAULT +
984+
' environment variable)')
985+
_raise_exception_for_reading_json(default_credential_filename,
986+
extra_help, error)
987+
elif well_known_file:
988+
try:
989+
return _get_default_credential_from_file(well_known_file)
990+
except (DefaultCredentialsError, ValueError) as error:
991+
extra_help = (' (produced automatically when running'
992+
' "gcloud auth login" command)')
993+
_raise_exception_for_reading_json(well_known_file, extra_help, error)
994+
elif _env_name in ('GAE_PRODUCTION', 'GAE_LOCAL'):
995+
return _get_default_credential_GAE()
996+
elif _env_name == 'GCE_PRODUCTION':
997+
return _get_default_credential_GCE()
998+
else:
999+
raise DefaultCredentialsError(
1000+
"The Default Credentials are not available. They are available if "
1001+
"running in Google App Engine or Google Compute Engine. They are "
1002+
"also available if using the Google Cloud SDK and running 'gcloud "
1003+
"auth login'. Otherwise, the environment variable " +
1004+
GOOGLE_CREDENTIALS_DEFAULT + " must be defined pointing to a file "
1005+
"defining the credentials. "
1006+
"See https://developers.google.com/accounts/docs/default-credentials "
1007+
"for details.")
1008+
1009+
@staticmethod
1010+
def from_stream(credential_filename):
1011+
"""Create a Credentials object by reading the information from a given file.
1012+
1013+
It returns an object of type GoogleCredentials.
1014+
1015+
Args:
1016+
credential_filename: the path to the file from where the credentials
1017+
are to be read
1018+
1019+
Exceptions:
1020+
DefaultCredentialsError: raised when the credentials fail to be retrieved.
1021+
"""
1022+
1023+
if credential_filename and os.path.isfile(credential_filename):
1024+
try:
1025+
return _get_default_credential_from_file(credential_filename)
1026+
except (DefaultCredentialsError, ValueError) as error:
1027+
extra_help = ' (provided as parameter to the from_stream() method)'
1028+
_raise_exception_for_reading_json(credential_filename,
1029+
extra_help,
1030+
error)
1031+
else:
1032+
raise DefaultCredentialsError('The parameter passed to the from_stream()'
1033+
' method should point to a file.')
1034+
1035+
1036+
def _get_environment_variable_file():
1037+
default_credential_filename = os.environ.get(GOOGLE_CREDENTIALS_DEFAULT,
1038+
None)
1039+
1040+
if default_credential_filename:
1041+
if os.path.isfile(default_credential_filename):
1042+
return default_credential_filename
1043+
else:
1044+
raise DefaultCredentialsError(
1045+
'File ' + default_credential_filename + ' (pointed by ' +
1046+
GOOGLE_CREDENTIALS_DEFAULT + ' environment variable) does not exist!')
1047+
1048+
1049+
def _get_well_known_file():
1050+
"""Get the well known file produced by command 'gcloud auth login'."""
1051+
# TODO(orestica): Revisit this method once gcloud provides a better way
1052+
# of pinpointing the exact location of the file.
1053+
1054+
WELL_KNOWN_CREDENTIALS_FILE = 'credentials_default.json'
1055+
CLOUDSDK_CONFIG_DIRECTORY = 'gcloud'
1056+
1057+
if os.name == 'nt':
1058+
try:
1059+
default_config_path = os.path.join(os.environ['APPDATA'],
1060+
CLOUDSDK_CONFIG_DIRECTORY)
1061+
except KeyError:
1062+
# This should never happen unless someone is really messing with things.
1063+
drive = os.environ.get('SystemDrive', 'C:')
1064+
default_config_path = os.path.join(drive, '\\', CLOUDSDK_CONFIG_DIRECTORY)
1065+
else:
1066+
default_config_path = os.path.join(os.path.expanduser('~'),
1067+
'.config',
1068+
CLOUDSDK_CONFIG_DIRECTORY)
1069+
1070+
default_config_path = os.path.join(default_config_path,
1071+
WELL_KNOWN_CREDENTIALS_FILE)
1072+
1073+
if os.path.isfile(default_config_path):
1074+
return default_config_path
1075+
1076+
1077+
def _get_default_credential_from_file(default_credential_filename):
1078+
"""Build the Default Credentials from file."""
1079+
1080+
import service_account
1081+
1082+
# read the credentials from the file
1083+
with open(default_credential_filename) as default_credential:
1084+
client_credentials = service_account.simplejson.load(default_credential)
1085+
1086+
credentials_type = client_credentials.get('type')
1087+
if credentials_type == AUTHORIZED_USER:
1088+
required_fields = set(['client_id', 'client_secret', 'refresh_token'])
1089+
elif credentials_type == SERVICE_ACCOUNT:
1090+
required_fields = set(['client_id', 'client_email', 'private_key_id',
1091+
'private_key'])
1092+
else:
1093+
raise DefaultCredentialsError("'type' field should be defined "
1094+
"(and have one of the '" + AUTHORIZED_USER +
1095+
"' or '" + SERVICE_ACCOUNT + "' values)")
1096+
1097+
missing_fields = required_fields.difference(client_credentials.keys())
1098+
1099+
if missing_fields:
1100+
_raise_exception_for_missing_fields(missing_fields)
1101+
1102+
if client_credentials['type'] == AUTHORIZED_USER:
1103+
return GoogleCredentials(
1104+
access_token=None,
1105+
client_id=client_credentials['client_id'],
1106+
client_secret=client_credentials['client_secret'],
1107+
refresh_token=client_credentials['refresh_token'],
1108+
token_expiry=None,
1109+
token_uri=GOOGLE_TOKEN_URI,
1110+
user_agent='Python client library')
1111+
else: # client_credentials['type'] == SERVICE_ACCOUNT
1112+
return service_account._ServiceAccountCredentials(
1113+
service_account_id=client_credentials['client_id'],
1114+
service_account_email=client_credentials['client_email'],
1115+
private_key_id=client_credentials['private_key_id'],
1116+
private_key_pkcs8_text=client_credentials['private_key'],
1117+
scopes=[])
1118+
1119+
1120+
def _raise_exception_for_missing_fields(missing_fields):
1121+
raise DefaultCredentialsError('The following field(s): ' +
1122+
', '.join(missing_fields) + ' must be defined.')
1123+
1124+
1125+
def _raise_exception_for_reading_json(credential_file,
1126+
extra_help,
1127+
error):
1128+
raise DefaultCredentialsError('An error was encountered while reading '
1129+
'json file: '+ credential_file + extra_help +
1130+
': ' + str(error))
1131+
1132+
1133+
def _get_default_credential_GAE():
1134+
from oauth2client.appengine import AppAssertionCredentials
1135+
1136+
return AppAssertionCredentials([])
1137+
1138+
1139+
def _get_default_credential_GCE():
1140+
from oauth2client.gce import AppAssertionCredentials
1141+
1142+
return AppAssertionCredentials([])
1143+
1144+
1145+
class AssertionCredentials(GoogleCredentials):
8241146
"""Abstract Credentials object used for OAuth 2.0 assertion grants.
8251147
8261148
This credential does not require a flow to instantiate because it
@@ -899,7 +1221,7 @@ class SignedJwtAssertionCredentials(AssertionCredentials):
8991221
later. For App Engine you may also consider using AppAssertionCredentials.
9001222
"""
9011223

902-
MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
1224+
MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
9031225

9041226
@util.positional(4)
9051227
def __init__(self,

0 commit comments

Comments
 (0)