Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(116)

Side by Side Diff: third_party/google-endpoints/apitools/base/py/credentials_lib.py

Issue 2666783008: Add google-endpoints to third_party/. (Closed)
Patch Set: Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 #
3 # Copyright 2015 Google Inc.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 """Common credentials classes and constructors."""
18 from __future__ import print_function
19
20 import datetime
21 import json
22 import os
23 import threading
24
25 import httplib2
26 import oauth2client
27 import oauth2client.client
28 import oauth2client.gce
29 import oauth2client.locked_file
30 import oauth2client.multistore_file
31 import oauth2client.service_account
32 from oauth2client import tools # for gflags declarations
33 from six.moves import http_client
34 from six.moves import urllib
35
36 from apitools.base.py import exceptions
37 from apitools.base.py import util
38
39 try:
40 # pylint: disable=wrong-import-order
41 import gflags
42 FLAGS = gflags.FLAGS
43 except ImportError:
44 FLAGS = None
45
46
47 __all__ = [
48 'CredentialsFromFile',
49 'GaeAssertionCredentials',
50 'GceAssertionCredentials',
51 'GetCredentials',
52 'GetUserinfo',
53 'ServiceAccountCredentials',
54 'ServiceAccountCredentialsFromFile',
55 ]
56
57
58 # Lock when accessing the cache file to avoid resource contention.
59 cache_file_lock = threading.Lock()
60
61
62 def SetCredentialsCacheFileLock(lock):
63 global cache_file_lock # pylint: disable=global-statement
64 cache_file_lock = lock
65
66
67 # List of additional methods we use when attempting to construct
68 # credentials. Users can register their own methods here, which we try
69 # before the defaults.
70 _CREDENTIALS_METHODS = []
71
72
73 def _RegisterCredentialsMethod(method, position=None):
74 """Register a new method for fetching credentials.
75
76 This new method should be a function with signature:
77 client_info, **kwds -> Credentials or None
78 This method can be used as a decorator, unless position needs to
79 be supplied.
80
81 Note that method must *always* accept arbitrary keyword arguments.
82
83 Args:
84 method: New credential-fetching method.
85 position: (default: None) Where in the list of methods to
86 add this; if None, we append. In all but rare cases,
87 this should be either 0 or None.
88 Returns:
89 method, for use as a decorator.
90
91 """
92 if position is None:
93 position = len(_CREDENTIALS_METHODS)
94 else:
95 position = min(position, len(_CREDENTIALS_METHODS))
96 _CREDENTIALS_METHODS.insert(position, method)
97 return method
98
99
100 def GetCredentials(package_name, scopes, client_id, client_secret, user_agent,
101 credentials_filename=None,
102 api_key=None, # pylint: disable=unused-argument
103 client=None, # pylint: disable=unused-argument
104 oauth2client_args=None,
105 **kwds):
106 """Attempt to get credentials, using an oauth dance as the last resort."""
107 scopes = util.NormalizeScopes(scopes)
108 client_info = {
109 'client_id': client_id,
110 'client_secret': client_secret,
111 'scope': ' '.join(sorted(scopes)),
112 'user_agent': user_agent or '%s-generated/0.1' % package_name,
113 }
114 for method in _CREDENTIALS_METHODS:
115 credentials = method(client_info, **kwds)
116 if credentials is not None:
117 return credentials
118 credentials_filename = credentials_filename or os.path.expanduser(
119 '~/.apitools.token')
120 credentials = CredentialsFromFile(credentials_filename, client_info,
121 oauth2client_args=oauth2client_args)
122 if credentials is not None:
123 return credentials
124 raise exceptions.CredentialsError('Could not create valid credentials')
125
126
127 def ServiceAccountCredentialsFromFile(
128 service_account_name, private_key_filename, scopes,
129 service_account_kwargs=None):
130 with open(private_key_filename) as key_file:
131 return ServiceAccountCredentials(
132 service_account_name, key_file.read(), scopes,
133 service_account_kwargs=service_account_kwargs)
134
135
136 def ServiceAccountCredentials(service_account_name, private_key, scopes,
137 service_account_kwargs=None):
138 service_account_kwargs = service_account_kwargs or {}
139 scopes = util.NormalizeScopes(scopes)
140 return oauth2client.client.SignedJwtAssertionCredentials(
141 service_account_name, private_key, scopes, **service_account_kwargs)
142
143
144 def _EnsureFileExists(filename):
145 """Touches a file; returns False on error, True on success."""
146 if not os.path.exists(filename):
147 old_umask = os.umask(0o177)
148 try:
149 open(filename, 'a+b').close()
150 except OSError:
151 return False
152 finally:
153 os.umask(old_umask)
154 return True
155
156
157 def _GceMetadataRequest(relative_url, use_metadata_ip=False):
158 """Request the given url from the GCE metadata service."""
159 if use_metadata_ip:
160 base_url = 'http://169.254.169.254/'
161 else:
162 base_url = 'http://metadata.google.internal/'
163 url = base_url + 'computeMetadata/v1/' + relative_url
164 # Extra header requirement can be found here:
165 # https://developers.google.com/compute/docs/metadata
166 headers = {'Metadata-Flavor': 'Google'}
167 request = urllib.request.Request(url, headers=headers)
168 opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
169 try:
170 response = opener.open(request)
171 except urllib.error.URLError as e:
172 raise exceptions.CommunicationError(
173 'Could not reach metadata service: %s' % e.reason)
174 return response
175
176
177 class GceAssertionCredentials(oauth2client.gce.AppAssertionCredentials):
178
179 """Assertion credentials for GCE instances."""
180
181 def __init__(self, scopes=None, service_account_name='default', **kwds):
182 """Initializes the credentials instance.
183
184 Args:
185 scopes: The scopes to get. If None, whatever scopes that are
186 available to the instance are used.
187 service_account_name: The service account to retrieve the scopes
188 from.
189 **kwds: Additional keyword args.
190
191 """
192 # If there is a connectivity issue with the metadata server,
193 # detection calls may fail even if we've already successfully
194 # identified these scopes in the same execution. However, the
195 # available scopes don't change once an instance is created,
196 # so there is no reason to perform more than one query.
197 self.__service_account_name = service_account_name
198 cached_scopes = None
199 cache_filename = kwds.get('cache_filename')
200 if cache_filename:
201 cached_scopes = self._CheckCacheFileForMatch(
202 cache_filename, scopes)
203
204 scopes = cached_scopes or self._ScopesFromMetadataServer(scopes)
205
206 if cache_filename and not cached_scopes:
207 self._WriteCacheFile(cache_filename, scopes)
208
209 super(GceAssertionCredentials, self).__init__(scopes, **kwds)
210
211 @classmethod
212 def Get(cls, *args, **kwds):
213 try:
214 return cls(*args, **kwds)
215 except exceptions.Error:
216 return None
217
218 def _CheckCacheFileForMatch(self, cache_filename, scopes):
219 """Checks the cache file to see if it matches the given credentials.
220
221 Args:
222 cache_filename: Cache filename to check.
223 scopes: Scopes for the desired credentials.
224
225 Returns:
226 List of scopes (if cache matches) or None.
227 """
228 creds = { # Credentials metadata dict.
229 'scopes': sorted(list(scopes)) if scopes else None,
230 'svc_acct_name': self.__service_account_name,
231 }
232 with cache_file_lock:
233 if _EnsureFileExists(cache_filename):
234 locked_file = oauth2client.locked_file.LockedFile(
235 cache_filename, 'r+b', 'rb')
236 try:
237 locked_file.open_and_lock()
238 cached_creds_str = locked_file.file_handle().read()
239 if cached_creds_str:
240 # Cached credentials metadata dict.
241 cached_creds = json.loads(cached_creds_str)
242 if (creds['svc_acct_name'] ==
243 cached_creds['svc_acct_name']):
244 if (creds['scopes'] in
245 (None, cached_creds['scopes'])):
246 scopes = cached_creds['scopes']
247 finally:
248 locked_file.unlock_and_close()
249 return scopes
250
251 def _WriteCacheFile(self, cache_filename, scopes):
252 """Writes the credential metadata to the cache file.
253
254 This does not save the credentials themselves (CredentialStore class
255 optionally handles that after this class is initialized).
256
257 Args:
258 cache_filename: Cache filename to check.
259 scopes: Scopes for the desired credentials.
260 """
261 with cache_file_lock:
262 if _EnsureFileExists(cache_filename):
263 locked_file = oauth2client.locked_file.LockedFile(
264 cache_filename, 'r+b', 'rb')
265 try:
266 locked_file.open_and_lock()
267 if locked_file.is_locked():
268 creds = { # Credentials metadata dict.
269 'scopes': sorted(list(scopes)),
270 'svc_acct_name': self.__service_account_name}
271 locked_file.file_handle().write(
272 json.dumps(creds, encoding='ascii'))
273 # If it's not locked, the locking process will
274 # write the same data to the file, so just
275 # continue.
276 finally:
277 locked_file.unlock_and_close()
278
279 def _ScopesFromMetadataServer(self, scopes):
280 if not util.DetectGce():
281 raise exceptions.ResourceUnavailableError(
282 'GCE credentials requested outside a GCE instance')
283 if not self.GetServiceAccount(self.__service_account_name):
284 raise exceptions.ResourceUnavailableError(
285 'GCE credentials requested but service account '
286 '%s does not exist.' % self.__service_account_name)
287 if scopes:
288 scope_ls = util.NormalizeScopes(scopes)
289 instance_scopes = self.GetInstanceScopes()
290 if scope_ls > instance_scopes:
291 raise exceptions.CredentialsError(
292 'Instance did not have access to scopes %s' % (
293 sorted(list(scope_ls - instance_scopes)),))
294 else:
295 scopes = self.GetInstanceScopes()
296 return scopes
297
298 def GetServiceAccount(self, account):
299 relative_url = 'instance/service-accounts'
300 response = _GceMetadataRequest(relative_url)
301 response_lines = [line.rstrip('/\n\r')
302 for line in response.readlines()]
303 return account in response_lines
304
305 def GetInstanceScopes(self):
306 relative_url = 'instance/service-accounts/{0}/scopes'.format(
307 self.__service_account_name)
308 response = _GceMetadataRequest(relative_url)
309 return util.NormalizeScopes(scope.strip()
310 for scope in response.readlines())
311
312 def _refresh(self, do_request):
313 """Refresh self.access_token.
314
315 This function replaces AppAssertionCredentials._refresh, which
316 does not use the credential store and is therefore poorly
317 suited for multi-threaded scenarios.
318
319 Args:
320 do_request: A function matching httplib2.Http.request's signature.
321
322 """
323 # pylint: disable=protected-access
324 oauth2client.client.OAuth2Credentials._refresh(self, do_request)
325 # pylint: enable=protected-access
326
327 def _do_refresh_request(self, unused_http_request):
328 """Refresh self.access_token by querying the metadata server.
329
330 If self.store is initialized, store acquired credentials there.
331 """
332 relative_url = 'instance/service-accounts/{0}/token'.format(
333 self.__service_account_name)
334 try:
335 response = _GceMetadataRequest(relative_url)
336 except exceptions.CommunicationError:
337 self.invalid = True
338 if self.store:
339 self.store.locked_put(self)
340 raise
341 content = response.read()
342 try:
343 credential_info = json.loads(content)
344 except ValueError:
345 raise exceptions.CredentialsError(
346 'Could not parse response as JSON: %s' % content)
347
348 self.access_token = credential_info['access_token']
349 if 'expires_in' in credential_info:
350 expires_in = int(credential_info['expires_in'])
351 self.token_expiry = (
352 datetime.timedelta(seconds=expires_in) +
353 datetime.datetime.utcnow())
354 else:
355 self.token_expiry = None
356 self.invalid = False
357 if self.store:
358 self.store.locked_put(self)
359
360 @classmethod
361 def from_json(cls, json_data):
362 data = json.loads(json_data)
363 kwargs = {}
364 if 'cache_filename' in data.get('kwargs', []):
365 kwargs['cache_filename'] = data['kwargs']['cache_filename']
366 credentials = GceAssertionCredentials(scopes=[data['scope']],
367 **kwargs)
368 if 'access_token' in data:
369 credentials.access_token = data['access_token']
370 if 'token_expiry' in data:
371 credentials.token_expiry = datetime.datetime.strptime(
372 data['token_expiry'], oauth2client.client.EXPIRY_FORMAT)
373 if 'invalid' in data:
374 credentials.invalid = data['invalid']
375 return credentials
376
377 @property
378 def serialization_data(self):
379 raise NotImplementedError(
380 'Cannot serialize credentials for GCE service accounts.')
381
382
383 # TODO(craigcitro): Currently, we can't even *load*
384 # `oauth2client.appengine` without being on appengine, because of how
385 # it handles imports. Fix that by splitting that module into
386 # GAE-specific and GAE-independent bits, and guarding imports.
387 class GaeAssertionCredentials(oauth2client.client.AssertionCredentials):
388
389 """Assertion credentials for Google App Engine apps."""
390
391 def __init__(self, scopes, **kwds):
392 if not util.DetectGae():
393 raise exceptions.ResourceUnavailableError(
394 'GCE credentials requested outside a GCE instance')
395 self._scopes = list(util.NormalizeScopes(scopes))
396 super(GaeAssertionCredentials, self).__init__(None, **kwds)
397
398 @classmethod
399 def Get(cls, *args, **kwds):
400 try:
401 return cls(*args, **kwds)
402 except exceptions.Error:
403 return None
404
405 @classmethod
406 def from_json(cls, json_data):
407 data = json.loads(json_data)
408 return GaeAssertionCredentials(data['_scopes'])
409
410 def _refresh(self, _):
411 """Refresh self.access_token.
412
413 Args:
414 _: (ignored) A function matching httplib2.Http.request's signature.
415 """
416 # pylint: disable=import-error
417 from google.appengine.api import app_identity
418 try:
419 token, _ = app_identity.get_access_token(self._scopes)
420 except app_identity.Error as e:
421 raise exceptions.CredentialsError(str(e))
422 self.access_token = token
423
424
425 def _GetRunFlowFlags(args=None):
426 # There's one rare situation where gsutil will not have argparse
427 # available, but doesn't need anything depending on argparse anyway,
428 # since they're bringing their own credentials. So we just allow this
429 # to fail with an ImportError in those cases.
430 #
431 # TODO(craigcitro): Move this import back to the top when we drop
432 # python 2.6 support (eg when gsutil does).
433 import argparse
434
435 parser = argparse.ArgumentParser(parents=[tools.argparser])
436 # Get command line argparse flags.
437 flags, _ = parser.parse_known_args(args=args)
438
439 # Allow `gflags` and `argparse` to be used side-by-side.
440 if hasattr(FLAGS, 'auth_host_name'):
441 flags.auth_host_name = FLAGS.auth_host_name
442 if hasattr(FLAGS, 'auth_host_port'):
443 flags.auth_host_port = FLAGS.auth_host_port
444 if hasattr(FLAGS, 'auth_local_webserver'):
445 flags.noauth_local_webserver = (not FLAGS.auth_local_webserver)
446 return flags
447
448
449 # TODO(craigcitro): Switch this from taking a path to taking a stream.
450 def CredentialsFromFile(path, client_info, oauth2client_args=None):
451 """Read credentials from a file."""
452 credential_store = oauth2client.multistore_file.get_credential_storage(
453 path,
454 client_info['client_id'],
455 client_info['user_agent'],
456 client_info['scope'])
457 if hasattr(FLAGS, 'auth_local_webserver'):
458 FLAGS.auth_local_webserver = False
459 credentials = credential_store.get()
460 if credentials is None or credentials.invalid:
461 print('Generating new OAuth credentials ...')
462 for _ in range(20):
463 # If authorization fails, we want to retry, rather than let this
464 # cascade up and get caught elsewhere. If users want out of the
465 # retry loop, they can ^C.
466 try:
467 flow = oauth2client.client.OAuth2WebServerFlow(**client_info)
468 flags = _GetRunFlowFlags(args=oauth2client_args)
469 credentials = tools.run_flow(flow, credential_store, flags)
470 break
471 except (oauth2client.client.FlowExchangeError, SystemExit) as e:
472 # Here SystemExit is "no credential at all", and the
473 # FlowExchangeError is "invalid" -- usually because
474 # you reused a token.
475 print('Invalid authorization: %s' % (e,))
476 except httplib2.HttpLib2Error as e:
477 print('Communication error: %s' % (e,))
478 raise exceptions.CredentialsError(
479 'Communication error creating credentials: %s' % e)
480 return credentials
481
482
483 # TODO(craigcitro): Push this into oauth2client.
484 def GetUserinfo(credentials, http=None): # pylint: disable=invalid-name
485 """Get the userinfo associated with the given credentials.
486
487 This is dependent on the token having either the userinfo.email or
488 userinfo.profile scope for the given token.
489
490 Args:
491 credentials: (oauth2client.client.Credentials) incoming credentials
492 http: (httplib2.Http, optional) http instance to use
493
494 Returns:
495 The email address for this token, or None if the required scopes
496 aren't available.
497 """
498 http = http or httplib2.Http()
499 url_root = 'https://www.googleapis.com/oauth2/v2/tokeninfo'
500 query_args = {'access_token': credentials.access_token}
501 url = '?'.join((url_root, urllib.parse.urlencode(query_args)))
502 # We ignore communication woes here (i.e. SSL errors, socket
503 # timeout), as handling these should be done in a common location.
504 response, content = http.request(url)
505 if response.status == http_client.BAD_REQUEST:
506 credentials.refresh(http)
507 response, content = http.request(url)
508 return json.loads(content or '{}') # Save ourselves from an empty reply.
509
510
511 @_RegisterCredentialsMethod
512 def _GetServiceAccountCredentials(
513 client_info, service_account_name=None, service_account_keyfile=None,
514 service_account_json_keyfile=None, **unused_kwds):
515 if ((service_account_name and not service_account_keyfile) or
516 (service_account_keyfile and not service_account_name)):
517 raise exceptions.CredentialsError(
518 'Service account name or keyfile provided without the other')
519 scopes = client_info['scope'].split()
520 user_agent = client_info['user_agent']
521 if service_account_json_keyfile:
522 with open(service_account_json_keyfile) as keyfile:
523 service_account_info = json.load(keyfile)
524 account_type = service_account_info.get('type')
525 if account_type != oauth2client.client.SERVICE_ACCOUNT:
526 raise exceptions.CredentialsError(
527 'Invalid service account credentials: %s' % (
528 service_account_json_keyfile,))
529 # pylint: disable=protected-access
530 credentials = oauth2client.service_account._ServiceAccountCredentials(
531 service_account_id=service_account_info['client_id'],
532 service_account_email=service_account_info['client_email'],
533 private_key_id=service_account_info['private_key_id'],
534 private_key_pkcs8_text=service_account_info['private_key'],
535 scopes=scopes, user_agent=user_agent)
536 # pylint: enable=protected-access
537 return credentials
538 if service_account_name is not None:
539 # pylint: disable=redefined-variable-type
540 credentials = ServiceAccountCredentialsFromFile(
541 service_account_name, service_account_keyfile, scopes,
542 service_account_kwargs={'user_agent': user_agent})
543 if credentials is not None:
544 return credentials
545
546
547 @_RegisterCredentialsMethod
548 def _GetGaeServiceAccount(client_info, **unused_kwds):
549 scopes = client_info['scope'].split(' ')
550 return GaeAssertionCredentials.Get(scopes=scopes)
551
552
553 @_RegisterCredentialsMethod
554 def _GetGceServiceAccount(client_info, **unused_kwds):
555 scopes = client_info['scope'].split(' ')
556 return GceAssertionCredentials.Get(scopes=scopes)
557
558
559 @_RegisterCredentialsMethod
560 def _GetApplicationDefaultCredentials(
561 client_info, skip_application_default_credentials=False,
562 **unused_kwds):
563 scopes = client_info['scope'].split()
564 if skip_application_default_credentials:
565 return None
566 gc = oauth2client.client.GoogleCredentials
567 with cache_file_lock:
568 try:
569 # pylint: disable=protected-access
570 # We've already done our own check for GAE/GCE
571 # credentials, we don't want to pay for checking again.
572 credentials = gc._implicit_credentials_from_files()
573 except oauth2client.client.ApplicationDefaultCredentialsError:
574 return None
575 # If we got back a non-service account credential, we need to use
576 # a heuristic to decide whether or not the application default
577 # credential will work for us. We assume that if we're requesting
578 # cloud-platform, our scopes are a subset of cloud scopes, and the
579 # ADC will work.
580 cp = 'https://www.googleapis.com/auth/cloud-platform'
581 if not isinstance(credentials, gc) or cp in scopes:
582 return credentials
583 return None
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698