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

Side by Side Diff: third_party/gcs-oauth2-boto-plugin/gcs_oauth2_boto_plugin/oauth2_client.py

Issue 698893003: Update checked in version of gsutil to version 4.6 (Closed) Base URL: http://dart.googlecode.com/svn/third_party/gsutil/
Patch Set: Created 6 years, 1 month 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 | Annotate | Revision Log
Property Changes:
Added: svn:eol-style
+ LF
OLDNEW
(Empty)
1 # Copyright 2014 Google Inc. All Rights Reserved.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 """An OAuth2 client library.
16
17 This library provides a client implementation of the OAuth2 protocol (see
18 https://developers.google.com/storage/docs/authentication.html#oauth).
19
20 **** Experimental API ****
21
22 This module is experimental and is subject to modification or removal without
23 notice.
24 """
25
26 # This implementation is a wrapper around the oauth2client implementation
27 # that implements caching of access tokens independent of refresh
28 # tokens (in the python API client oauth2client, there is a single class that
29 # encapsulates both refresh and access tokens).
30
31 from __future__ import absolute_import
32
33 import cgi
34 import datetime
35 import errno
36 from hashlib import sha1
37 import json
38 import logging
39 import os
40 import socket
41 import tempfile
42 import threading
43 import urllib
44
45 if os.environ.get('USER_AGENT'):
46 import boto
47 boto.UserAgent += os.environ.get('USER_AGENT')
48
49 from boto import config
50 import httplib2
51 from oauth2client.client import AccessTokenRefreshError
52 from oauth2client.client import HAS_CRYPTO
53 from oauth2client.client import OAuth2Credentials
54 from retry_decorator.retry_decorator import retry as Retry
55 import socks
56
57 if HAS_CRYPTO:
58 from oauth2client.client import SignedJwtAssertionCredentials
59
60 LOG = logging.getLogger('oauth2_client')
61
62 # Lock used for checking/exchanging refresh token, so multithreaded
63 # operation doesn't attempt concurrent refreshes.
64 token_exchange_lock = threading.Lock()
65
66 DEFAULT_SCOPE = 'https://www.googleapis.com/auth/devstorage.full_control'
67
68 METADATA_SERVER = 'http://metadata.google.internal'
69
70 META_TOKEN_URI = (METADATA_SERVER + '/computeMetadata/v1/instance/'
71 'service-accounts/default/token')
72
73 META_HEADERS = {
74 'X-Google-Metadata-Request': 'True'
75 }
76
77
78 # Note: this is copied from gsutil's gslib.cred_types. It should be kept in
79 # sync. Also note that this library does not use HMAC, but it's preserved from
80 # gsutil's copy to maintain compatibility.
81 class CredTypes(object):
82 HMAC = "HMAC"
83 OAUTH2_SERVICE_ACCOUNT = "OAuth 2.0 Service Account"
84 OAUTH2_USER_ACCOUNT = "Oauth 2.0 User Account"
85 GCE = "GCE"
86
87
88 class Error(Exception):
89 """Base exception for the OAuth2 module."""
90 pass
91
92
93 class AccessTokenRefreshError(Error):
94 """Error trying to exchange a refresh token into an access token."""
95 pass
96
97
98 class AuthorizationCodeExchangeError(Error):
99 """Error trying to exchange an authorization code into a refresh token."""
100 pass
101
102
103 class TokenCache(object):
104 """Interface for OAuth2 token caches."""
105
106 def PutToken(self, key, value):
107 raise NotImplementedError
108
109 def GetToken(self, key):
110 raise NotImplementedError
111
112
113 class NoopTokenCache(TokenCache):
114 """A stub implementation of TokenCache that does nothing."""
115
116 def PutToken(self, key, value):
117 pass
118
119 def GetToken(self, key):
120 return None
121
122
123 class InMemoryTokenCache(TokenCache):
124 """An in-memory token cache.
125
126 The cache is implemented by a python dict, and inherits the thread-safety
127 properties of dict.
128 """
129
130 def __init__(self):
131 super(InMemoryTokenCache, self).__init__()
132 self.cache = dict()
133
134 def PutToken(self, key, value):
135 LOG.debug('InMemoryTokenCache.PutToken: key=%s', key)
136 self.cache[key] = value
137
138 def GetToken(self, key):
139 value = self.cache.get(key, None)
140 LOG.debug('InMemoryTokenCache.GetToken: key=%s%s present',
141 key, ' not' if value is None else '')
142 return value
143
144
145 class FileSystemTokenCache(TokenCache):
146 """An implementation of a token cache that persists tokens on disk.
147
148 Each token object in the cache is stored in serialized form in a separate
149 file. The cache file's name can be configured via a path pattern that is
150 parameterized by the key under which a value is cached and optionally the
151 current processes uid as obtained by os.getuid().
152
153 Since file names are generally publicly visible in the system, it is important
154 that the cache key does not leak information about the token's value. If
155 client code computes cache keys from token values, a cryptographically strong
156 one-way function must be used.
157 """
158
159 def __init__(self, path_pattern=None):
160 """Creates a FileSystemTokenCache.
161
162 Args:
163 path_pattern: Optional string argument to specify the path pattern for
164 cache files. The argument should be a path with format placeholders
165 '%(key)s' and optionally '%(uid)s'. If the argument is omitted, the
166 default pattern
167 <tmpdir>/oauth2client-tokencache.%(uid)s.%(key)s
168 is used, where <tmpdir> is replaced with the system temp dir as
169 obtained from tempfile.gettempdir().
170 """
171 super(FileSystemTokenCache, self).__init__()
172 self.path_pattern = path_pattern
173 if not path_pattern:
174 self.path_pattern = os.path.join(
175 tempfile.gettempdir(), 'oauth2_client-tokencache.%(uid)s.%(key)s')
176
177 def CacheFileName(self, key):
178 uid = '_'
179 try:
180 # os.getuid() doesn't seem to work in Windows
181 uid = str(os.getuid())
182 except:
183 pass
184 return self.path_pattern % {'key': key, 'uid': uid}
185
186 def PutToken(self, key, value):
187 """Serializes the value to the key's filename.
188
189 To ensure that written tokens aren't leaked to a different users, we
190 a) unlink an existing cache file, if any (to ensure we don't fall victim
191 to symlink attacks and the like),
192 b) create a new file with O_CREAT | O_EXCL (to ensure nobody is trying to
193 race us)
194 If either of these steps fail, we simply give up (but log a warning). Not
195 caching access tokens is not catastrophic, and failure to create a file
196 can happen for either of the following reasons:
197 - someone is attacking us as above, in which case we want to default to
198 safe operation (not write the token);
199 - another legitimate process is racing us; in this case one of the two
200 will win and write the access token, which is fine;
201 - we don't have permission to remove the old file or write to the
202 specified directory, in which case we can't recover
203
204 Args:
205 key: the hash key to store.
206 value: the access_token value to serialize.
207 """
208
209 cache_file = self.CacheFileName(key)
210 LOG.debug('FileSystemTokenCache.PutToken: key=%s, cache_file=%s',
211 key, cache_file)
212 try:
213 os.unlink(cache_file)
214 except:
215 # Ignore failure to unlink the file; if the file exists and can't be
216 # unlinked, the subsequent open with O_CREAT | O_EXCL will fail.
217 pass
218
219 flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
220
221 # Accommodate Windows; stolen from python2.6/tempfile.py.
222 if hasattr(os, 'O_NOINHERIT'):
223 flags |= os.O_NOINHERIT
224 if hasattr(os, 'O_BINARY'):
225 flags |= os.O_BINARY
226
227 try:
228 fd = os.open(cache_file, flags, 0600)
229 except (OSError, IOError) as e:
230 LOG.warning('FileSystemTokenCache.PutToken: '
231 'Failed to create cache file %s: %s', cache_file, e)
232 return
233 f = os.fdopen(fd, 'w+b')
234 f.write(value.Serialize())
235 f.close()
236
237 def GetToken(self, key):
238 """Returns a deserialized access token from the key's filename."""
239 value = None
240 cache_file = self.CacheFileName(key)
241
242 try:
243 f = open(cache_file)
244 value = AccessToken.UnSerialize(f.read())
245 f.close()
246 except (IOError, OSError) as e:
247 if e.errno != errno.ENOENT:
248 LOG.warning('FileSystemTokenCache.GetToken: '
249 'Failed to read cache file %s: %s', cache_file, e)
250 except Exception as e:
251 LOG.warning('FileSystemTokenCache.GetToken: '
252 'Failed to read cache file %s (possibly corrupted): %s',
253 cache_file, e)
254
255 LOG.debug('FileSystemTokenCache.GetToken: key=%s%s present (cache_file=%s)',
256 key, ' not' if value is None else '', cache_file)
257 return value
258
259
260 class OAuth2Client(object):
261 """Common logic for OAuth2 clients."""
262
263 def __init__(self, cache_key_base, access_token_cache=None,
264 datetime_strategy=datetime.datetime, auth_uri=None,
265 token_uri=None, disable_ssl_certificate_validation=False,
266 proxy_host=None, proxy_port=None, proxy_user=None,
267 proxy_pass=None, ca_certs_file=None):
268 # datetime_strategy is used to invoke utcnow() on; it is injected into the
269 # constructor for unit testing purposes.
270 self.auth_uri = auth_uri
271 self.token_uri = token_uri
272 self.cache_key_base = cache_key_base
273 self.datetime_strategy = datetime_strategy
274 self.access_token_cache = access_token_cache or InMemoryTokenCache()
275 self.disable_ssl_certificate_validation = disable_ssl_certificate_validation
276 self.ca_certs_file = ca_certs_file
277 if proxy_host and proxy_port:
278 self._proxy_info = httplib2.ProxyInfo(socks.PROXY_TYPE_HTTP,
279 proxy_host,
280 proxy_port,
281 proxy_user=proxy_user,
282 proxy_pass=proxy_pass,
283 proxy_rdns=True)
284 else:
285 self._proxy_info = None
286
287 def CreateHttpRequest(self):
288 return httplib2.Http(
289 ca_certs=self.ca_certs_file,
290 disable_ssl_certificate_validation=(
291 self.disable_ssl_certificate_validation),
292 proxy_info=self._proxy_info)
293
294 def GetAccessToken(self):
295 """Obtains an access token for this client.
296
297 This client's access token cache is first checked for an existing,
298 not-yet-expired access token. If none is found, the client obtains a fresh
299 access token from the OAuth2 provider's token endpoint.
300
301 Returns:
302 The cached or freshly obtained AccessToken.
303 Raises:
304 AccessTokenRefreshError if an error occurs.
305 """
306 # Ensure only one thread at a time attempts to get (and possibly refresh)
307 # the access token. This doesn't prevent concurrent refresh attempts across
308 # multiple gsutil instances, but at least protects against multiple threads
309 # simultaneously attempting to refresh when gsutil -m is used.
310 token_exchange_lock.acquire()
311 try:
312 cache_key = self.CacheKey()
313 LOG.debug('GetAccessToken: checking cache for key %s', cache_key)
314 access_token = self.access_token_cache.GetToken(cache_key)
315 LOG.debug('GetAccessToken: token from cache: %s', access_token)
316 if access_token is None or access_token.ShouldRefresh():
317 LOG.debug('GetAccessToken: fetching fresh access token...')
318 access_token = self.FetchAccessToken()
319 LOG.debug('GetAccessToken: fresh access token: %s', access_token)
320 self.access_token_cache.PutToken(cache_key, access_token)
321 return access_token
322 finally:
323 token_exchange_lock.release()
324
325 def CacheKey(self):
326 """Computes a cache key.
327
328 The cache key is computed as the SHA1 hash of the refresh token for user
329 accounts, or the hash of the gs_service_client_id for service accounts,
330 which satisfies the FileSystemTokenCache requirement that cache keys do not
331 leak information about token values.
332
333 Returns:
334 A hash key.
335 """
336 h = sha1()
337 h.update(self.cache_key_base)
338 return h.hexdigest()
339
340 def GetAuthorizationHeader(self):
341 """Gets the access token HTTP authorization header value.
342
343 Returns:
344 The value of an Authorization HTTP header that authenticates
345 requests with an OAuth2 access token.
346 """
347 return 'Bearer %s' % self.GetAccessToken().token
348
349
350 class OAuth2ServiceAccountClient(OAuth2Client):
351
352 def __init__(self, client_id, private_key, password,
353 access_token_cache=None, auth_uri=None, token_uri=None,
354 datetime_strategy=datetime.datetime,
355 disable_ssl_certificate_validation=False,
356 proxy_host=None, proxy_port=None, proxy_user=None,
357 proxy_pass=None, ca_certs_file=None):
358 """Creates an OAuth2ServiceAccountClient.
359
360 Args:
361 client_id: The OAuth2 client ID of this client.
362 private_key: The private key associated with this service account.
363 password: The private key password used for the crypto signer.
364 access_token_cache: An optional instance of a TokenCache. If omitted or
365 None, an InMemoryTokenCache is used.
366 auth_uri: The URI for OAuth2 authorization.
367 token_uri: The URI used to refresh access tokens.
368 datetime_strategy: datetime module strategy to use.
369 disable_ssl_certificate_validation: True if certifications should not be
370 validated.
371 proxy_host: An optional string specifying the host name of an HTTP proxy
372 to be used.
373 proxy_port: An optional int specifying the port number of an HTTP proxy
374 to be used.
375 proxy_user: An optional string specifying the user name for interacting
376 with the HTTP proxy.
377 proxy_pass: An optional string specifying the password for interacting
378 with the HTTP proxy.
379 ca_certs_file: The cacerts.txt file to use.
380 """
381 super(OAuth2ServiceAccountClient, self).__init__(
382 cache_key_base=client_id, auth_uri=auth_uri, token_uri=token_uri,
383 access_token_cache=access_token_cache,
384 datetime_strategy=datetime_strategy,
385 disable_ssl_certificate_validation=disable_ssl_certificate_validation,
386 proxy_host=proxy_host, proxy_port=proxy_port, proxy_user=proxy_user,
387 proxy_pass=proxy_pass, ca_certs_file=ca_certs_file)
388 self.client_id = client_id
389 self.private_key = private_key
390 self.password = password
391
392 def FetchAccessToken(self):
393 credentials = self.GetCredentials()
394 http = self.CreateHttpRequest()
395 credentials.refresh(http)
396 return AccessToken(credentials.access_token,
397 credentials.token_expiry, datetime_strategy=self.datetime_strategy)
398
399 def GetCredentials(self):
400 if HAS_CRYPTO:
401 return SignedJwtAssertionCredentials(self.client_id,
402 self.private_key, scope=DEFAULT_SCOPE,
403 private_key_password=self.password)
404 else:
405 raise MissingDependencyError(
406 'Service account authentication requires PyOpenSSL. Please install '
407 'this library and try again.')
408
409
410 class GsAccessTokenRefreshError(Exception):
411 """Transient error when requesting access token."""
412 def __init__(self, e):
413 super(Exception, self).__init__(e)
414
415
416 class GsInvalidRefreshTokenError(Exception):
417 def __init__(self, e):
418 super(Exception, self).__init__(e)
419
420
421 class MissingDependencyError(Exception):
422 def __init__(self, e):
423 super(Exception, self).__init__(e)
424
425
426 class OAuth2UserAccountClient(OAuth2Client):
427 """An OAuth2 client."""
428
429 def __init__(self, token_uri, client_id, client_secret, refresh_token,
430 auth_uri=None, access_token_cache=None,
431 datetime_strategy=datetime.datetime,
432 disable_ssl_certificate_validation=False,
433 proxy_host=None, proxy_port=None, proxy_user=None,
434 proxy_pass=None, ca_certs_file=None):
435 """Creates an OAuth2UserAccountClient.
436
437 Args:
438 token_uri: The URI used to refresh access tokens.
439 client_id: The OAuth2 client ID of this client.
440 client_secret: The OAuth2 client secret of this client.
441 refresh_token: The token used to refresh the access token.
442 auth_uri: The URI for OAuth2 authorization.
443 access_token_cache: An optional instance of a TokenCache. If omitted or
444 None, an InMemoryTokenCache is used.
445 datetime_strategy: datetime module strategy to use.
446 disable_ssl_certificate_validation: True if certifications should not be
447 validated.
448 proxy_host: An optional string specifying the host name of an HTTP proxy
449 to be used.
450 proxy_port: An optional int specifying the port number of an HTTP proxy
451 to be used.
452 proxy_user: An optional string specifying the user name for interacting
453 with the HTTP proxy.
454 proxy_pass: An optional string specifying the password for interacting
455 with the HTTP proxy.
456 ca_certs_file: The cacerts.txt file to use.
457 """
458 super(OAuth2UserAccountClient, self).__init__(
459 cache_key_base=refresh_token, auth_uri=auth_uri, token_uri=token_uri,
460 access_token_cache=access_token_cache,
461 datetime_strategy=datetime_strategy,
462 disable_ssl_certificate_validation=disable_ssl_certificate_validation,
463 proxy_host=proxy_host, proxy_port=proxy_port, proxy_user=proxy_user,
464 proxy_pass=proxy_pass, ca_certs_file=ca_certs_file)
465 self.token_uri = token_uri
466 self.client_id = client_id
467 self.client_secret = client_secret
468 self.refresh_token = refresh_token
469
470 def GetCredentials(self):
471 """Fetches a credentials objects from the provider's token endpoint."""
472 access_token = self.GetAccessToken()
473 credentials = OAuth2Credentials(
474 access_token.token, self.client_id, self.client_secret,
475 self.refresh_token, access_token.expiry, self.token_uri, None)
476 return credentials
477
478 @Retry(GsAccessTokenRefreshError,
479 tries=config.get('OAuth2', 'oauth2_refresh_retries', 6),
480 timeout_secs=1)
481 def FetchAccessToken(self):
482 """Fetches an access token from the provider's token endpoint.
483
484 Fetches an access token from this client's OAuth2 provider's token endpoint.
485
486 Returns:
487 The fetched AccessToken.
488 """
489 try:
490 http = self.CreateHttpRequest()
491 credentials = OAuth2Credentials(None, self.client_id, self.client_secret,
492 self.refresh_token, None, self.token_uri, None)
493 credentials.refresh(http)
494 return AccessToken(credentials.access_token,
495 credentials.token_expiry, datetime_strategy=self.datetime_strategy)
496 except AccessTokenRefreshError, e:
497 if 'Invalid response 403' in e.message:
498 # This is the most we can do at the moment to accurately detect rate
499 # limiting errors since they come back as 403s with no further
500 # information.
501 raise GsAccessTokenRefreshError(e)
502 elif 'invalid_grant' in e.message:
503 LOG.info("""
504 Attempted to retrieve an access token from an invalid refresh token. Two common
505 cases in which you will see this error are:
506 1. Your refresh token was revoked.
507 2. Your refresh token was typed incorrectly.
508 """)
509 raise GsInvalidRefreshTokenError(e)
510 else:
511 raise
512
513
514 class OAuth2GCEClient(OAuth2Client):
515 """OAuth2 client for GCE instance."""
516
517 def __init__(self):
518 super(OAuth2GCEClient, self).__init__(
519 cache_key_base='',
520 # Only InMemoryTokenCache can be used with empty cache_key_base.
521 access_token_cache=InMemoryTokenCache())
522
523 @Retry(GsAccessTokenRefreshError,
524 tries=6,
525 timeout_secs=1)
526 def FetchAccessToken(self):
527 response = None
528 try:
529 http = httplib2.Http()
530 response, content = http.request(META_TOKEN_URI, method='GET',
531 body=None, headers=META_HEADERS)
532 except Exception:
533 raise GsAccessTokenRefreshError()
534
535 if response.status == 200:
536 d = json.loads(content)
537
538 return AccessToken(
539 d['access_token'],
540 datetime.datetime.now() +
541 datetime.timedelta(seconds=d.get('expires_in', 0)),
542 datetime_strategy=self.datetime_strategy)
543
544
545 def _IsGCE():
546 try:
547 http = httplib2.Http()
548 response, _ = http.request(METADATA_SERVER)
549 return response.status == 200
550
551 except (httplib2.ServerNotFoundError, socket.error):
552 # We might see something like "No route to host" propagated as a socket
553 # error. We might also catch transient socket errors, but at that point
554 # we're going to fail anyway, just with a different error message. With
555 # this approach, we'll avoid having to enumerate all possible non-transient
556 # socket errors.
557 return False
558 except Exception, e:
559 LOG.warning("Failed to determine whether we're running on GCE, so we'll"
560 "assume that we aren't: %s", e)
561 return False
562
563 return False
564
565
566 def CreateOAuth2GCEClient():
567 return OAuth2GCEClient() if _IsGCE() else None
568
569
570 class AccessToken(object):
571 """Encapsulates an OAuth2 access token."""
572
573 def __init__(self, token, expiry, datetime_strategy=datetime.datetime):
574 self.token = token
575 self.expiry = expiry
576 self.datetime_strategy = datetime_strategy
577
578 @staticmethod
579 def UnSerialize(query):
580 """Creates an AccessToken object from its serialized form."""
581
582 def GetValue(d, key):
583 return (d.get(key, [None]))[0]
584 kv = cgi.parse_qs(query)
585 if not kv['token']:
586 return None
587 expiry = None
588 expiry_tuple = GetValue(kv, 'expiry')
589 if expiry_tuple:
590 try:
591 expiry = datetime.datetime(
592 *[int(n) for n in expiry_tuple.split(',')])
593 except:
594 return None
595 return AccessToken(GetValue(kv, 'token'), expiry)
596
597 def Serialize(self):
598 """Serializes this object as URI-encoded key-value pairs."""
599 # There's got to be a better way to serialize a datetime. Unfortunately,
600 # there is no reliable way to convert into a unix epoch.
601 kv = {'token': self.token}
602 if self.expiry:
603 t = self.expiry
604 tupl = (t.year, t.month, t.day, t.hour, t.minute, t.second, t.microsecond)
605 kv['expiry'] = ','.join([str(i) for i in tupl])
606 return urllib.urlencode(kv)
607
608 def ShouldRefresh(self, time_delta=300):
609 """Whether the access token needs to be refreshed.
610
611 Args:
612 time_delta: refresh access token when it expires within time_delta secs.
613
614 Returns:
615 True if the token is expired or about to expire, False if the
616 token should be expected to work. Note that the token may still
617 be rejected, e.g. if it has been revoked server-side.
618 """
619 if self.expiry is None:
620 return False
621 return (self.datetime_strategy.utcnow()
622 + datetime.timedelta(seconds=time_delta) > self.expiry)
623
624 def __eq__(self, other):
625 return self.token == other.token and self.expiry == other.expiry
626
627 def __ne__(self, other):
628 return not self.__eq__(other)
629
630 def __str__(self):
631 return 'AccessToken(token=%s, expiry=%sZ)' % (self.token, self.expiry)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698