OLD | NEW |
(Empty) | |
| 1 diff --git a/third_party/google_api_python_client/apiclient/__init__.py b/third_
party/google_api_python_client/apiclient/__init__.py |
| 2 index 5efb142..acb1a23 100644 |
| 3 --- a/third_party/google_api_python_client/apiclient/__init__.py |
| 4 +++ b/third_party/google_api_python_client/apiclient/__init__.py |
| 5 @@ -3,7 +3,7 @@ |
| 6 import googleapiclient |
| 7 |
| 8 try: |
| 9 - import oauth2client |
| 10 + from third_party import oauth2client |
| 11 except ImportError: |
| 12 raise RuntimeError( |
| 13 'Previous version of google-api-python-client detected; due to a ' |
| 14 diff --git a/third_party/google_api_python_client/googleapiclient/channel.py b/t
hird_party/google_api_python_client/googleapiclient/channel.py |
| 15 index 68a3b89..4626094 100644 |
| 16 --- a/third_party/google_api_python_client/googleapiclient/channel.py |
| 17 +++ b/third_party/google_api_python_client/googleapiclient/channel.py |
| 18 @@ -60,7 +60,7 @@ import datetime |
| 19 import uuid |
| 20 |
| 21 from googleapiclient import errors |
| 22 -from ...oauth2client import util |
| 23 +from third_party.oauth2client import util |
| 24 |
| 25 |
| 26 # The unix time epoch starts at midnight 1970. |
| 27 diff --git a/third_party/google_api_python_client/googleapiclient/discovery.py b
/third_party/google_api_python_client/googleapiclient/discovery.py |
| 28 index 3ddac57..0e9e5cf 100644 |
| 29 --- a/third_party/google_api_python_client/googleapiclient/discovery.py |
| 30 +++ b/third_party/google_api_python_client/googleapiclient/discovery.py |
| 31 @@ -47,9 +47,9 @@ except ImportError: |
| 32 from cgi import parse_qsl |
| 33 |
| 34 # Third-party imports |
| 35 -from ... import httplib2 |
| 36 +from third_party import httplib2 |
| 37 +from third_party.uritemplate import uritemplate |
| 38 import mimeparse |
| 39 -from ... import uritemplate |
| 40 |
| 41 # Local imports |
| 42 from googleapiclient.errors import HttpError |
| 43 @@ -65,9 +65,9 @@ from googleapiclient.model import JsonModel |
| 44 from googleapiclient.model import MediaModel |
| 45 from googleapiclient.model import RawModel |
| 46 from googleapiclient.schema import Schemas |
| 47 -from oauth2client.client import GoogleCredentials |
| 48 -from oauth2client.util import _add_query_parameter |
| 49 -from oauth2client.util import positional |
| 50 +from third_party.oauth2client.client import GoogleCredentials |
| 51 +from third_party.oauth2client.util import _add_query_parameter |
| 52 +from third_party.oauth2client.util import positional |
| 53 |
| 54 |
| 55 # The client library requires a version of httplib2 that supports RETRIES. |
| 56 diff --git a/third_party/google_api_python_client/googleapiclient/errors.py b/th
ird_party/google_api_python_client/googleapiclient/errors.py |
| 57 index a1999fd..18c52e6 100644 |
| 58 --- a/third_party/google_api_python_client/googleapiclient/errors.py |
| 59 +++ b/third_party/google_api_python_client/googleapiclient/errors.py |
| 60 @@ -24,7 +24,7 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)' |
| 61 |
| 62 import json |
| 63 |
| 64 -from ...oauth2client import util |
| 65 +from third_party.oauth2client import util |
| 66 |
| 67 |
| 68 class Error(Exception): |
| 69 diff --git a/third_party/google_api_python_client/googleapiclient/http.py b/thir
d_party/google_api_python_client/googleapiclient/http.py |
| 70 index 8638279..d2ce70c 100644 |
| 71 --- a/third_party/google_api_python_client/googleapiclient/http.py |
| 72 +++ b/third_party/google_api_python_client/googleapiclient/http.py |
| 73 @@ -25,7 +25,6 @@ import StringIO |
| 74 import base64 |
| 75 import copy |
| 76 import gzip |
| 77 -import httplib2 |
| 78 import json |
| 79 import logging |
| 80 import mimeparse |
| 81 @@ -49,7 +48,8 @@ from errors import ResumableUploadError |
| 82 from errors import UnexpectedBodyError |
| 83 from errors import UnexpectedMethodError |
| 84 from model import JsonModel |
| 85 -from ...oauth2client import util |
| 86 +from third_party import httplib2 |
| 87 +from third_party.oauth2client import util |
| 88 |
| 89 |
| 90 DEFAULT_CHUNK_SIZE = 512*1024 |
| 91 diff --git a/third_party/google_api_python_client/googleapiclient/sample_tools.p
y b/third_party/google_api_python_client/googleapiclient/sample_tools.py |
| 92 index cbd6d6f..cc0790b 100644 |
| 93 --- a/third_party/google_api_python_client/googleapiclient/sample_tools.py |
| 94 +++ b/third_party/google_api_python_client/googleapiclient/sample_tools.py |
| 95 @@ -22,13 +22,13 @@ __all__ = ['init'] |
| 96 |
| 97 |
| 98 import argparse |
| 99 -import httplib2 |
| 100 import os |
| 101 |
| 102 from googleapiclient import discovery |
| 103 -from ...oauth2client import client |
| 104 -from ...oauth2client import file |
| 105 -from ...oauth2client import tools |
| 106 +from third_party import httplib2 |
| 107 +from third_party.oauth2client import client |
| 108 +from third_party.oauth2client import file |
| 109 +from third_party.oauth2client import tools |
| 110 |
| 111 |
| 112 def init(argv, name, version, doc, filename, scope=None, parents=[], discovery_
filename=None): |
| 113 diff --git a/third_party/google_api_python_client/googleapiclient/schema.py b/th
ird_party/google_api_python_client/googleapiclient/schema.py |
| 114 index af41317..92543ec 100644 |
| 115 --- a/third_party/google_api_python_client/googleapiclient/schema.py |
| 116 +++ b/third_party/google_api_python_client/googleapiclient/schema.py |
| 117 @@ -63,7 +63,7 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)' |
| 118 |
| 119 import copy |
| 120 |
| 121 -from oauth2client import util |
| 122 +from third_party.oauth2client import util |
| 123 |
| 124 |
| 125 class Schemas(object): |
| 126 diff --git a/third_party/oauth2client/README.chromium b/third_party/oauth2client
/README.chromium |
| 127 index ea15b96..bfc4a01 100644 |
| 128 --- a/third_party/oauth2client/README.chromium |
| 129 +++ b/third_party/oauth2client/README.chromium |
| 130 @@ -1,7 +1,7 @@ |
| 131 Name: oauth2client |
| 132 Short Name: oauth2client |
| 133 -URL: https://pypi.python.org/packages/source/o/oauth2client/oauth2client-1.2.ta
r.gz |
| 134 -Version: 1.2 |
| 135 +URL: https://pypi.python.org/packages/source/o/oauth2client/oauth2client-1.4.7.
tar.gz |
| 136 +Version: 1.4.7 |
| 137 License: Apache License 2.0 |
| 138 |
| 139 Description: |
| 140 diff --git a/third_party/oauth2client/__init__.py b/third_party/oauth2client/__i
nit__.py |
| 141 index ac84748..fea30ca 100644 |
| 142 --- a/third_party/oauth2client/__init__.py |
| 143 +++ b/third_party/oauth2client/__init__.py |
| 144 @@ -1,5 +1,8 @@ |
| 145 -__version__ = "1.2" |
| 146 +"""Client library for using OAuth2, especially with Google APIs.""" |
| 147 + |
| 148 +__version__ = '1.4.7' |
| 149 |
| 150 GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/auth' |
| 151 +GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code' |
| 152 GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke' |
| 153 -GOOGLE_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token' |
| 154 +GOOGLE_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token' |
| 155 \ No newline at end of file |
| 156 diff --git a/third_party/oauth2client/anyjson.py b/third_party/oauth2client/anyj
son.py |
| 157 deleted file mode 100644 |
| 158 index ae21c33..0000000 |
| 159 --- a/third_party/oauth2client/anyjson.py |
| 160 +++ /dev/null |
| 161 @@ -1,32 +0,0 @@ |
| 162 -# Copyright (C) 2010 Google Inc. |
| 163 -# |
| 164 -# Licensed under the Apache License, Version 2.0 (the "License"); |
| 165 -# you may not use this file except in compliance with the License. |
| 166 -# You may obtain a copy of the License at |
| 167 -# |
| 168 -# http://www.apache.org/licenses/LICENSE-2.0 |
| 169 -# |
| 170 -# Unless required by applicable law or agreed to in writing, software |
| 171 -# distributed under the License is distributed on an "AS IS" BASIS, |
| 172 -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 173 -# See the License for the specific language governing permissions and |
| 174 -# limitations under the License. |
| 175 - |
| 176 -"""Utility module to import a JSON module |
| 177 - |
| 178 -Hides all the messy details of exactly where |
| 179 -we get a simplejson module from. |
| 180 -""" |
| 181 - |
| 182 -__author__ = 'jcgregorio@google.com (Joe Gregorio)' |
| 183 - |
| 184 - |
| 185 -try: # pragma: no cover |
| 186 - # Should work for Python2.6 and higher. |
| 187 - import json as simplejson |
| 188 -except ImportError: # pragma: no cover |
| 189 - try: |
| 190 - import simplejson |
| 191 - except ImportError: |
| 192 - # Try to import from django, should work on App Engine |
| 193 - from django.utils import simplejson |
| 194 diff --git a/third_party/oauth2client/appengine.py b/third_party/oauth2client/ap
pengine.py |
| 195 index 5cd3f4b..4131513 100644 |
| 196 --- a/third_party/oauth2client/appengine.py |
| 197 +++ b/third_party/oauth2client/appengine.py |
| 198 @@ -1,4 +1,4 @@ |
| 199 -# Copyright (C) 2010 Google Inc. |
| 200 +# Copyright 2014 Google Inc. All rights reserved. |
| 201 # |
| 202 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 203 # you may not use this file except in compliance with the License. |
| 204 @@ -19,14 +19,14 @@ Utilities for making it easier to use OAuth 2.0 on Google Ap
p Engine. |
| 205 |
| 206 __author__ = 'jcgregorio@google.com (Joe Gregorio)' |
| 207 |
| 208 -import base64 |
| 209 import cgi |
| 210 -import httplib2 |
| 211 +import json |
| 212 import logging |
| 213 import os |
| 214 import pickle |
| 215 import threading |
| 216 -import time |
| 217 + |
| 218 +import httplib2 |
| 219 |
| 220 from google.appengine.api import app_identity |
| 221 from google.appengine.api import memcache |
| 222 @@ -41,7 +41,6 @@ from oauth2client import GOOGLE_TOKEN_URI |
| 223 from oauth2client import clientsecrets |
| 224 from oauth2client import util |
| 225 from oauth2client import xsrfutil |
| 226 -from oauth2client.anyjson import simplejson |
| 227 from oauth2client.client import AccessTokenRefreshError |
| 228 from oauth2client.client import AssertionCredentials |
| 229 from oauth2client.client import Credentials |
| 230 @@ -159,15 +158,20 @@ class AppAssertionCredentials(AssertionCredentials): |
| 231 Args: |
| 232 scope: string or iterable of strings, scope(s) of the credentials being |
| 233 requested. |
| 234 + **kwargs: optional keyword args, including: |
| 235 + service_account_id: service account id of the application. If None or |
| 236 + unspecified, the default service account for the app is used. |
| 237 """ |
| 238 self.scope = util.scopes_to_string(scope) |
| 239 + self._kwargs = kwargs |
| 240 + self.service_account_id = kwargs.get('service_account_id', None) |
| 241 |
| 242 # Assertion type is no longer used, but still in the parent class signature
. |
| 243 super(AppAssertionCredentials, self).__init__(None) |
| 244 |
| 245 @classmethod |
| 246 - def from_json(cls, json): |
| 247 - data = simplejson.loads(json) |
| 248 + def from_json(cls, json_data): |
| 249 + data = json.loads(json_data) |
| 250 return AppAssertionCredentials(data['scope']) |
| 251 |
| 252 def _refresh(self, http_request): |
| 253 @@ -186,11 +190,22 @@ class AppAssertionCredentials(AssertionCredentials): |
| 254 """ |
| 255 try: |
| 256 scopes = self.scope.split() |
| 257 - (token, _) = app_identity.get_access_token(scopes) |
| 258 - except app_identity.Error, e: |
| 259 + (token, _) = app_identity.get_access_token( |
| 260 + scopes, service_account_id=self.service_account_id) |
| 261 + except app_identity.Error as e: |
| 262 raise AccessTokenRefreshError(str(e)) |
| 263 self.access_token = token |
| 264 |
| 265 + @property |
| 266 + def serialization_data(self): |
| 267 + raise NotImplementedError('Cannot serialize credentials for AppEngine.') |
| 268 + |
| 269 + def create_scoped_required(self): |
| 270 + return not self.scope |
| 271 + |
| 272 + def create_scoped(self, scopes): |
| 273 + return AppAssertionCredentials(scopes, **self._kwargs) |
| 274 + |
| 275 |
| 276 class FlowProperty(db.Property): |
| 277 """App Engine datastore Property for Flow. |
| 278 @@ -434,6 +449,7 @@ class StorageByKeyName(Storage): |
| 279 entity_key = db.Key.from_path(self._model.kind(), self._key_name) |
| 280 db.delete(entity_key) |
| 281 |
| 282 + @db.non_transactional(allow_existing=True) |
| 283 def locked_get(self): |
| 284 """Retrieve Credential from datastore. |
| 285 |
| 286 @@ -456,6 +472,7 @@ class StorageByKeyName(Storage): |
| 287 credentials.set_store(self) |
| 288 return credentials |
| 289 |
| 290 + @db.non_transactional(allow_existing=True) |
| 291 def locked_put(self, credentials): |
| 292 """Write a Credentials to the datastore. |
| 293 |
| 294 @@ -468,6 +485,7 @@ class StorageByKeyName(Storage): |
| 295 if self._cache: |
| 296 self._cache.set(self._key_name, credentials.to_json()) |
| 297 |
| 298 + @db.non_transactional(allow_existing=True) |
| 299 def locked_delete(self): |
| 300 """Delete Credential from datastore.""" |
| 301 |
| 302 @@ -553,16 +571,14 @@ class OAuth2Decorator(object): |
| 303 Instantiate and then use with oauth_required or oauth_aware |
| 304 as decorators on webapp.RequestHandler methods. |
| 305 |
| 306 - Example: |
| 307 + :: |
| 308 |
| 309 decorator = OAuth2Decorator( |
| 310 client_id='837...ent.com', |
| 311 client_secret='Qh...wwI', |
| 312 scope='https://www.googleapis.com/auth/plus') |
| 313 |
| 314 - |
| 315 class MainHandler(webapp.RequestHandler): |
| 316 - |
| 317 @decorator.oauth_required |
| 318 def get(self): |
| 319 http = decorator.http() |
| 320 @@ -650,8 +666,9 @@ class OAuth2Decorator(object): |
| 321 provided to this constructor. A string indicating the name of the field |
| 322 on the _credentials_class where a Credentials object will be stored. |
| 323 Defaults to 'credentials'. |
| 324 - **kwargs: dict, Keyword arguments are be passed along as kwargs to the |
| 325 - OAuth2WebServerFlow constructor. |
| 326 + **kwargs: dict, Keyword arguments are passed along as kwargs to |
| 327 + the OAuth2WebServerFlow constructor. |
| 328 + |
| 329 """ |
| 330 self._tls = threading.local() |
| 331 self.flow = None |
| 332 @@ -798,14 +815,18 @@ class OAuth2Decorator(object): |
| 333 url = self.flow.step1_get_authorize_url() |
| 334 return str(url) |
| 335 |
| 336 - def http(self): |
| 337 + def http(self, *args, **kwargs): |
| 338 """Returns an authorized http instance. |
| 339 |
| 340 Must only be called from within an @oauth_required decorated method, or |
| 341 from within an @oauth_aware decorated method where has_credentials() |
| 342 returns True. |
| 343 + |
| 344 + Args: |
| 345 + *args: Positional arguments passed to httplib2.Http constructor. |
| 346 + **kwargs: Positional arguments passed to httplib2.Http constructor. |
| 347 """ |
| 348 - return self.credentials.authorize(httplib2.Http()) |
| 349 + return self.credentials.authorize(httplib2.Http(*args, **kwargs)) |
| 350 |
| 351 @property |
| 352 def callback_path(self): |
| 353 @@ -824,7 +845,8 @@ class OAuth2Decorator(object): |
| 354 def callback_handler(self): |
| 355 """RequestHandler for the OAuth 2.0 redirect callback. |
| 356 |
| 357 - Usage: |
| 358 + Usage:: |
| 359 + |
| 360 app = webapp.WSGIApplication([ |
| 361 ('/index', MyIndexHandler), |
| 362 ..., |
| 363 @@ -858,7 +880,7 @@ class OAuth2Decorator(object): |
| 364 user) |
| 365 |
| 366 if decorator._token_response_param and credentials.token_response: |
| 367 - resp_json = simplejson.dumps(credentials.token_response) |
| 368 + resp_json = json.dumps(credentials.token_response) |
| 369 redirect_uri = util._add_query_parameter( |
| 370 redirect_uri, decorator._token_response_param, resp_json) |
| 371 |
| 372 @@ -887,24 +909,23 @@ class OAuth2DecoratorFromClientSecrets(OAuth2Decorator): |
| 373 Uses a clientsecrets file as the source for all the information when |
| 374 constructing an OAuth2Decorator. |
| 375 |
| 376 - Example: |
| 377 + :: |
| 378 |
| 379 decorator = OAuth2DecoratorFromClientSecrets( |
| 380 os.path.join(os.path.dirname(__file__), 'client_secrets.json') |
| 381 scope='https://www.googleapis.com/auth/plus') |
| 382 |
| 383 - |
| 384 class MainHandler(webapp.RequestHandler): |
| 385 - |
| 386 @decorator.oauth_required |
| 387 def get(self): |
| 388 http = decorator.http() |
| 389 # http is authorized with the user's Credentials and can be used |
| 390 # in API calls |
| 391 + |
| 392 """ |
| 393 |
| 394 @util.positional(3) |
| 395 - def __init__(self, filename, scope, message=None, cache=None): |
| 396 + def __init__(self, filename, scope, message=None, cache=None, **kwargs): |
| 397 """Constructor |
| 398 |
| 399 Args: |
| 400 @@ -917,17 +938,20 @@ class OAuth2DecoratorFromClientSecrets(OAuth2Decorator): |
| 401 decorator. |
| 402 cache: An optional cache service client that implements get() and set() |
| 403 methods. See clientsecrets.loadfile() for details. |
| 404 + **kwargs: dict, Keyword arguments are passed along as kwargs to |
| 405 + the OAuth2WebServerFlow constructor. |
| 406 """ |
| 407 client_type, client_info = clientsecrets.loadfile(filename, cache=cache) |
| 408 if client_type not in [ |
| 409 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]: |
| 410 raise InvalidClientSecretsError( |
| 411 - 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.') |
| 412 - constructor_kwargs = { |
| 413 - 'auth_uri': client_info['auth_uri'], |
| 414 - 'token_uri': client_info['token_uri'], |
| 415 - 'message': message, |
| 416 - } |
| 417 + "OAuth2Decorator doesn't support this OAuth 2.0 flow.") |
| 418 + constructor_kwargs = dict(kwargs) |
| 419 + constructor_kwargs.update({ |
| 420 + 'auth_uri': client_info['auth_uri'], |
| 421 + 'token_uri': client_info['token_uri'], |
| 422 + 'message': message, |
| 423 + }) |
| 424 revoke_uri = client_info.get('revoke_uri') |
| 425 if revoke_uri is not None: |
| 426 constructor_kwargs['revoke_uri'] = revoke_uri |
| 427 @@ -960,4 +984,4 @@ def oauth2decorator_from_clientsecrets(filename, scope, |
| 428 |
| 429 """ |
| 430 return OAuth2DecoratorFromClientSecrets(filename, scope, |
| 431 - message=message, cache=cache) |
| 432 + message=message, cache=cache) |
| 433 \ No newline at end of file |
| 434 diff --git a/third_party/oauth2client/client.py b/third_party/oauth2client/clien
t.py |
| 435 index 6901f3f..8fe28a3 100644 |
| 436 --- a/third_party/oauth2client/client.py |
| 437 +++ b/third_party/oauth2client/client.py |
| 438 @@ -1,4 +1,4 @@ |
| 439 -# Copyright (C) 2010 Google Inc. |
| 440 +# Copyright 2014 Google Inc. All rights reserved. |
| 441 # |
| 442 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 443 # you may not use this file except in compliance with the License. |
| 444 @@ -20,44 +20,48 @@ Tools for interacting with OAuth 2.0 protected resources. |
| 445 __author__ = 'jcgregorio@google.com (Joe Gregorio)' |
| 446 |
| 447 import base64 |
| 448 -import clientsecrets |
| 449 +import collections |
| 450 import copy |
| 451 import datetime |
| 452 -from .. import httplib2 |
| 453 +import json |
| 454 import logging |
| 455 +import os |
| 456 +import socket |
| 457 import sys |
| 458 +import tempfile |
| 459 import time |
| 460 -import urllib |
| 461 -import urlparse |
| 462 +import shutil |
| 463 |
| 464 +from .. import httplib2 |
| 465 +from . import clientsecrets |
| 466 from . import GOOGLE_AUTH_URI |
| 467 +from . import GOOGLE_DEVICE_URI |
| 468 from . import GOOGLE_REVOKE_URI |
| 469 from . import GOOGLE_TOKEN_URI |
| 470 from . import util |
| 471 -from .anyjson import simplejson |
| 472 +from third_party import six |
| 473 +from third_party.six.moves import urllib |
| 474 |
| 475 HAS_OPENSSL = False |
| 476 HAS_CRYPTO = False |
| 477 try: |
| 478 - from . import crypt |
| 479 + from oauth2client import crypt |
| 480 HAS_CRYPTO = True |
| 481 if crypt.OpenSSLVerifier is not None: |
| 482 HAS_OPENSSL = True |
| 483 except ImportError: |
| 484 pass |
| 485 |
| 486 -try: |
| 487 - from urlparse import parse_qsl |
| 488 -except ImportError: |
| 489 - from cgi import parse_qsl |
| 490 - |
| 491 logger = logging.getLogger(__name__) |
| 492 |
| 493 # Expiry is stored in RFC3339 UTC format |
| 494 EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ' |
| 495 |
| 496 # Which certs to use to validate id_tokens received. |
| 497 -ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs' |
| 498 +ID_TOKEN_VERIFICATION_CERTS = 'https://www.googleapis.com/oauth2/v1/certs' |
| 499 +# This symbol previously had a typo in the name; we keep the old name |
| 500 +# around for now, but will remove it in the future. |
| 501 +ID_TOKEN_VERIFICATON_CERTS = ID_TOKEN_VERIFICATION_CERTS |
| 502 |
| 503 # Constant to use for the out of band OAuth 2.0 flow. |
| 504 OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob' |
| 505 @@ -65,6 +69,42 @@ OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob' |
| 506 # Google Data client libraries may need to set this to [401, 403]. |
| 507 REFRESH_STATUS_CODES = [401] |
| 508 |
| 509 +# The value representing user credentials. |
| 510 +AUTHORIZED_USER = 'authorized_user' |
| 511 + |
| 512 +# The value representing service account credentials. |
| 513 +SERVICE_ACCOUNT = 'service_account' |
| 514 + |
| 515 +# The environment variable pointing the file with local |
| 516 +# Application Default Credentials. |
| 517 +GOOGLE_APPLICATION_CREDENTIALS = 'GOOGLE_APPLICATION_CREDENTIALS' |
| 518 +# The ~/.config subdirectory containing gcloud credentials. Intended |
| 519 +# to be swapped out in tests. |
| 520 +_CLOUDSDK_CONFIG_DIRECTORY = 'gcloud' |
| 521 + |
| 522 +# The error message we show users when we can't find the Application |
| 523 +# Default Credentials. |
| 524 +ADC_HELP_MSG = ( |
| 525 + 'The Application Default Credentials are not available. They are available
' |
| 526 + 'if running in Google Compute Engine. Otherwise, the environment variable ' |
| 527 + + GOOGLE_APPLICATION_CREDENTIALS + |
| 528 + ' must be defined pointing to a file defining the credentials. See ' |
| 529 + 'https://developers.google.com/accounts/docs/application-default-credential
s' # pylint:disable=line-too-long |
| 530 + ' for more information.') |
| 531 + |
| 532 +# The access token along with the seconds in which it expires. |
| 533 +AccessTokenInfo = collections.namedtuple( |
| 534 + 'AccessTokenInfo', ['access_token', 'expires_in']) |
| 535 + |
| 536 +DEFAULT_ENV_NAME = 'UNKNOWN' |
| 537 + |
| 538 +# If set to True _get_environment avoid GCE check (_detect_gce_environment) |
| 539 +NO_GCE_CHECK = os.environ.setdefault('NO_GCE_CHECK', 'False') |
| 540 + |
| 541 +class SETTINGS(object): |
| 542 + """Settings namespace for globally defined values.""" |
| 543 + env_name = None |
| 544 + |
| 545 |
| 546 class Error(Exception): |
| 547 """Base error for this module.""" |
| 548 @@ -91,13 +131,25 @@ class AccessTokenCredentialsError(Error): |
| 549 |
| 550 |
| 551 class VerifyJwtTokenError(Error): |
| 552 - """Could on retrieve certificates for validation.""" |
| 553 + """Could not retrieve certificates for validation.""" |
| 554 |
| 555 |
| 556 class NonAsciiHeaderError(Error): |
| 557 """Header names and values must be ASCII strings.""" |
| 558 |
| 559 |
| 560 +class ApplicationDefaultCredentialsError(Error): |
| 561 + """Error retrieving the Application Default Credentials.""" |
| 562 + |
| 563 + |
| 564 +class OAuth2DeviceCodeError(Error): |
| 565 + """Error trying to retrieve a device code.""" |
| 566 + |
| 567 + |
| 568 +class CryptoUnavailableError(Error, NotImplementedError): |
| 569 + """Raised when a crypto library is required, but none is available.""" |
| 570 + |
| 571 + |
| 572 def _abstract(): |
| 573 raise NotImplementedError('You need to override this function') |
| 574 |
| 575 @@ -125,11 +177,12 @@ class Credentials(object): |
| 576 an HTTP transport. |
| 577 |
| 578 Subclasses must also specify a classmethod named 'from_json' that takes a JSO
N |
| 579 - string as input and returns an instaniated Credentials object. |
| 580 + string as input and returns an instantiated Credentials object. |
| 581 """ |
| 582 |
| 583 NON_SERIALIZED_MEMBERS = ['store'] |
| 584 |
| 585 + |
| 586 def authorize(self, http): |
| 587 """Take an httplib2.Http instance (or equivalent) and authorizes it. |
| 588 |
| 589 @@ -143,6 +196,7 @@ class Credentials(object): |
| 590 """ |
| 591 _abstract() |
| 592 |
| 593 + |
| 594 def refresh(self, http): |
| 595 """Forces a refresh of the access_token. |
| 596 |
| 597 @@ -152,6 +206,7 @@ class Credentials(object): |
| 598 """ |
| 599 _abstract() |
| 600 |
| 601 + |
| 602 def revoke(self, http): |
| 603 """Revokes a refresh_token and makes the credentials void. |
| 604 |
| 605 @@ -161,6 +216,7 @@ class Credentials(object): |
| 606 """ |
| 607 _abstract() |
| 608 |
| 609 + |
| 610 def apply(self, headers): |
| 611 """Add the authorization to the headers. |
| 612 |
| 613 @@ -184,12 +240,16 @@ class Credentials(object): |
| 614 for member in strip: |
| 615 if member in d: |
| 616 del d[member] |
| 617 - if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime)
: |
| 618 + if (d.get('token_expiry') and |
| 619 + isinstance(d['token_expiry'], datetime.datetime)): |
| 620 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT) |
| 621 # Add in information we will need later to reconsistitue this instance. |
| 622 d['_class'] = t.__name__ |
| 623 d['_module'] = t.__module__ |
| 624 - return simplejson.dumps(d) |
| 625 + for key, val in d.items(): |
| 626 + if isinstance(val, bytes): |
| 627 + d[key] = val.decode('utf-8') |
| 628 + return json.dumps(d) |
| 629 |
| 630 def to_json(self): |
| 631 """Creating a JSON representation of an instance of Credentials. |
| 632 @@ -212,14 +272,16 @@ class Credentials(object): |
| 633 An instance of the subclass of Credentials that was serialized with |
| 634 to_json(). |
| 635 """ |
| 636 - data = simplejson.loads(s) |
| 637 + if six.PY3 and isinstance(s, bytes): |
| 638 + s = s.decode('utf-8') |
| 639 + data = json.loads(s) |
| 640 # Find and call the right classmethod from_json() to restore the object. |
| 641 module = data['_module'] |
| 642 try: |
| 643 m = __import__(module) |
| 644 except ImportError: |
| 645 # In case there's an object from the old package structure, update it |
| 646 - module = module.replace('.apiclient', '') |
| 647 + module = module.replace('.googleapiclient', '') |
| 648 m = __import__(module) |
| 649 |
| 650 m = __import__(module, fromlist=module.split('.')[:-1]) |
| 651 @@ -228,13 +290,13 @@ class Credentials(object): |
| 652 return from_json(s) |
| 653 |
| 654 @classmethod |
| 655 - def from_json(cls, s): |
| 656 + def from_json(cls, unused_data): |
| 657 """Instantiate a Credentials object from a JSON description of it. |
| 658 |
| 659 The JSON should have been produced by calling .to_json() on the object. |
| 660 |
| 661 Args: |
| 662 - data: dict, A deserialized JSON object. |
| 663 + unused_data: dict, A deserialized JSON object. |
| 664 |
| 665 Returns: |
| 666 An instance of a Credentials subclass. |
| 667 @@ -356,8 +418,10 @@ def clean_headers(headers): |
| 668 """ |
| 669 clean = {} |
| 670 try: |
| 671 - for k, v in headers.iteritems(): |
| 672 - clean[str(k)] = str(v) |
| 673 + for k, v in six.iteritems(headers): |
| 674 + clean_k = k if isinstance(k, bytes) else str(k).encode('ascii') |
| 675 + clean_v = v if isinstance(v, bytes) else str(v).encode('ascii') |
| 676 + clean[clean_k] = clean_v |
| 677 except UnicodeEncodeError: |
| 678 raise NonAsciiHeaderError(k + ': ' + v) |
| 679 return clean |
| 680 @@ -373,11 +437,11 @@ def _update_query_params(uri, params): |
| 681 Returns: |
| 682 The same URI but with the new query parameters added. |
| 683 """ |
| 684 - parts = list(urlparse.urlparse(uri)) |
| 685 - query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part |
| 686 + parts = urllib.parse.urlparse(uri) |
| 687 + query_params = dict(urllib.parse.parse_qsl(parts.query)) |
| 688 query_params.update(params) |
| 689 - parts[4] = urllib.urlencode(query_params) |
| 690 - return urlparse.urlunparse(parts) |
| 691 + new_parts = parts._replace(query=urllib.parse.urlencode(query_params)) |
| 692 + return urllib.parse.urlunparse(new_parts) |
| 693 |
| 694 |
| 695 class OAuth2Credentials(Credentials): |
| 696 @@ -445,22 +509,23 @@ class OAuth2Credentials(Credentials): |
| 697 it. |
| 698 |
| 699 Args: |
| 700 - http: An instance of httplib2.Http |
| 701 - or something that acts like it. |
| 702 + http: An instance of ``httplib2.Http`` or something that acts |
| 703 + like it. |
| 704 |
| 705 Returns: |
| 706 A modified instance of http that was passed in. |
| 707 |
| 708 - Example: |
| 709 + Example:: |
| 710 |
| 711 h = httplib2.Http() |
| 712 h = credentials.authorize(h) |
| 713 |
| 714 - You can't create a new OAuth subclass of httplib2.Authenication |
| 715 + You can't create a new OAuth subclass of httplib2.Authentication |
| 716 because it never gets passed the absolute URI, which is needed for |
| 717 signing. So instead we have to overload 'request' with a closure |
| 718 that adds in the Authorization header and then calls the original |
| 719 version of 'request()'. |
| 720 + |
| 721 """ |
| 722 request_orig = http.request |
| 723 |
| 724 @@ -473,10 +538,12 @@ class OAuth2Credentials(Credentials): |
| 725 logger.info('Attempting refresh to obtain initial access_token') |
| 726 self._refresh(request_orig) |
| 727 |
| 728 - # Modify the request headers to add the appropriate |
| 729 + # Clone and modify the request headers to add the appropriate |
| 730 # Authorization header. |
| 731 if headers is None: |
| 732 headers = {} |
| 733 + else: |
| 734 + headers = dict(headers) |
| 735 self.apply(headers) |
| 736 |
| 737 if self.user_agent is not None: |
| 738 @@ -489,7 +556,7 @@ class OAuth2Credentials(Credentials): |
| 739 redirections, connection_type) |
| 740 |
| 741 if resp.status in REFRESH_STATUS_CODES: |
| 742 - logger.info('Refreshing due to a %s' % str(resp.status)) |
| 743 + logger.info('Refreshing due to a %s', resp.status) |
| 744 self._refresh(request_orig) |
| 745 self.apply(headers) |
| 746 return request_orig(uri, method, body, clean_headers(headers), |
| 747 @@ -545,13 +612,15 @@ class OAuth2Credentials(Credentials): |
| 748 Returns: |
| 749 An instance of a Credentials subclass. |
| 750 """ |
| 751 - data = simplejson.loads(s) |
| 752 - if 'token_expiry' in data and not isinstance(data['token_expiry'], |
| 753 - datetime.datetime): |
| 754 + if six.PY3 and isinstance(s, bytes): |
| 755 + s = s.decode('utf-8') |
| 756 + data = json.loads(s) |
| 757 + if (data.get('token_expiry') and |
| 758 + not isinstance(data['token_expiry'], datetime.datetime)): |
| 759 try: |
| 760 data['token_expiry'] = datetime.datetime.strptime( |
| 761 data['token_expiry'], EXPIRY_FORMAT) |
| 762 - except: |
| 763 + except ValueError: |
| 764 data['token_expiry'] = None |
| 765 retval = cls( |
| 766 data['access_token'], |
| 767 @@ -586,11 +655,24 @@ class OAuth2Credentials(Credentials): |
| 768 return True |
| 769 return False |
| 770 |
| 771 + def get_access_token(self, http=None): |
| 772 + """Return the access token and its expiration information. |
| 773 + |
| 774 + If the token does not exist, get one. |
| 775 + If the token expired, refresh it. |
| 776 + """ |
| 777 + if not self.access_token or self.access_token_expired: |
| 778 + if not http: |
| 779 + http = httplib2.Http() |
| 780 + self.refresh(http) |
| 781 + return AccessTokenInfo(access_token=self.access_token, |
| 782 + expires_in=self._expires_in()) |
| 783 + |
| 784 def set_store(self, store): |
| 785 """Set the Storage for the credential. |
| 786 |
| 787 Args: |
| 788 - store: Storage, an implementation of Stroage object. |
| 789 + store: Storage, an implementation of Storage object. |
| 790 This is needed to store the latest access_token if it |
| 791 has expired and been refreshed. This implementation uses |
| 792 locking to check for updates before updating the |
| 793 @@ -598,6 +680,25 @@ class OAuth2Credentials(Credentials): |
| 794 """ |
| 795 self.store = store |
| 796 |
| 797 + def _expires_in(self): |
| 798 + """Return the number of seconds until this token expires. |
| 799 + |
| 800 + If token_expiry is in the past, this method will return 0, meaning the |
| 801 + token has already expired. |
| 802 + If token_expiry is None, this method will return None. Note that returning |
| 803 + 0 in such a case would not be fair: the token may still be valid; |
| 804 + we just don't know anything about it. |
| 805 + """ |
| 806 + if self.token_expiry: |
| 807 + now = datetime.datetime.utcnow() |
| 808 + if self.token_expiry > now: |
| 809 + time_delta = self.token_expiry - now |
| 810 + # TODO(orestica): return time_delta.total_seconds() |
| 811 + # once dropping support for Python 2.6 |
| 812 + return time_delta.days * 86400 + time_delta.seconds |
| 813 + else: |
| 814 + return 0 |
| 815 + |
| 816 def _updateFromCredential(self, other): |
| 817 """Update this Credential from another instance.""" |
| 818 self.__dict__.update(other.__getstate__()) |
| 819 @@ -615,7 +716,7 @@ class OAuth2Credentials(Credentials): |
| 820 |
| 821 def _generate_refresh_request_body(self): |
| 822 """Generate the body that will be used in the refresh request.""" |
| 823 - body = urllib.urlencode({ |
| 824 + body = urllib.parse.urlencode({ |
| 825 'grant_type': 'refresh_token', |
| 826 'client_id': self.client_id, |
| 827 'client_secret': self.client_secret, |
| 828 @@ -679,9 +780,10 @@ class OAuth2Credentials(Credentials): |
| 829 logger.info('Refreshing access_token') |
| 830 resp, content = http_request( |
| 831 self.token_uri, method='POST', body=body, headers=headers) |
| 832 + if six.PY3 and isinstance(content, bytes): |
| 833 + content = content.decode('utf-8') |
| 834 if resp.status == 200: |
| 835 - # TODO(jcgregorio) Raise an error if loads fails? |
| 836 - d = simplejson.loads(content) |
| 837 + d = json.loads(content) |
| 838 self.token_response = d |
| 839 self.access_token = d['access_token'] |
| 840 self.refresh_token = d.get('refresh_token', self.refresh_token) |
| 841 @@ -690,35 +792,40 @@ class OAuth2Credentials(Credentials): |
| 842 seconds=int(d['expires_in'])) + datetime.datetime.utcnow() |
| 843 else: |
| 844 self.token_expiry = None |
| 845 + # On temporary refresh errors, the user does not actually have to |
| 846 + # re-authorize, so we unflag here. |
| 847 + self.invalid = False |
| 848 if self.store: |
| 849 self.store.locked_put(self) |
| 850 else: |
| 851 # An {'error':...} response body means the token is expired or revoked, |
| 852 # so we flag the credentials as such. |
| 853 - logger.info('Failed to retrieve access token: %s' % content) |
| 854 + logger.info('Failed to retrieve access token: %s', content) |
| 855 error_msg = 'Invalid response %s.' % resp['status'] |
| 856 try: |
| 857 - d = simplejson.loads(content) |
| 858 + d = json.loads(content) |
| 859 if 'error' in d: |
| 860 error_msg = d['error'] |
| 861 + if 'error_description' in d: |
| 862 + error_msg += ': ' + d['error_description'] |
| 863 self.invalid = True |
| 864 if self.store: |
| 865 self.store.locked_put(self) |
| 866 - except StandardError: |
| 867 + except (TypeError, ValueError): |
| 868 pass |
| 869 raise AccessTokenRefreshError(error_msg) |
| 870 |
| 871 def _revoke(self, http_request): |
| 872 - """Revokes the refresh_token and deletes the store if available. |
| 873 + """Revokes this credential and deletes the stored copy (if it exists). |
| 874 |
| 875 Args: |
| 876 http_request: callable, a callable that matches the method signature of |
| 877 httplib2.Http.request, used to make the revoke request. |
| 878 """ |
| 879 - self._do_revoke(http_request, self.refresh_token) |
| 880 + self._do_revoke(http_request, self.refresh_token or self.access_token) |
| 881 |
| 882 def _do_revoke(self, http_request, token): |
| 883 - """Revokes the credentials and deletes the store if available. |
| 884 + """Revokes this credential and deletes the stored copy (if it exists). |
| 885 |
| 886 Args: |
| 887 http_request: callable, a callable that matches the method signature of |
| 888 @@ -738,10 +845,10 @@ class OAuth2Credentials(Credentials): |
| 889 else: |
| 890 error_msg = 'Invalid response %s.' % resp.status |
| 891 try: |
| 892 - d = simplejson.loads(content) |
| 893 + d = json.loads(content) |
| 894 if 'error' in d: |
| 895 error_msg = d['error'] |
| 896 - except StandardError: |
| 897 + except (TypeError, ValueError): |
| 898 pass |
| 899 raise TokenRevokeError(error_msg) |
| 900 |
| 901 @@ -763,7 +870,8 @@ class AccessTokenCredentials(OAuth2Credentials): |
| 902 |
| 903 AccessTokenCredentials objects may be safely pickled and unpickled. |
| 904 |
| 905 - Usage: |
| 906 + Usage:: |
| 907 + |
| 908 credentials = AccessTokenCredentials('<an access token>', |
| 909 'my-user-agent/1.0') |
| 910 http = httplib2.Http() |
| 911 @@ -799,10 +907,12 @@ class AccessTokenCredentials(OAuth2Credentials): |
| 912 |
| 913 @classmethod |
| 914 def from_json(cls, s): |
| 915 - data = simplejson.loads(s) |
| 916 + if six.PY3 and isinstance(s, bytes): |
| 917 + s = s.decode('utf-8') |
| 918 + data = json.loads(s) |
| 919 retval = AccessTokenCredentials( |
| 920 - data['access_token'], |
| 921 - data['user_agent']) |
| 922 + data['access_token'], |
| 923 + data['user_agent']) |
| 924 return retval |
| 925 |
| 926 def _refresh(self, http_request): |
| 927 @@ -819,7 +929,434 @@ class AccessTokenCredentials(OAuth2Credentials): |
| 928 self._do_revoke(http_request, self.access_token) |
| 929 |
| 930 |
| 931 -class AssertionCredentials(OAuth2Credentials): |
| 932 +def _detect_gce_environment(urlopen=None): |
| 933 + """Determine if the current environment is Compute Engine. |
| 934 + |
| 935 + Args: |
| 936 + urlopen: Optional argument. Function used to open a connection to a URL. |
| 937 + |
| 938 + Returns: |
| 939 + Boolean indicating whether or not the current environment is Google |
| 940 + Compute Engine. |
| 941 + """ |
| 942 + urlopen = urlopen or urllib.request.urlopen |
| 943 + # Note: the explicit `timeout` below is a workaround. The underlying |
| 944 + # issue is that resolving an unknown host on some networks will take |
| 945 + # 20-30 seconds; making this timeout short fixes the issue, but |
| 946 + # could lead to false negatives in the event that we are on GCE, but |
| 947 + # the metadata resolution was particularly slow. The latter case is |
| 948 + # "unlikely". |
| 949 + try: |
| 950 + response = urlopen('http://169.254.169.254/', timeout=1) |
| 951 + return response.info().get('Metadata-Flavor', '') == 'Google' |
| 952 + except socket.timeout: |
| 953 + logger.info('Timeout attempting to reach GCE metadata service.') |
| 954 + return False |
| 955 + except urllib.error.URLError as e: |
| 956 + if isinstance(getattr(e, 'reason', None), socket.timeout): |
| 957 + logger.info('Timeout attempting to reach GCE metadata service.') |
| 958 + return False |
| 959 + |
| 960 + |
| 961 +def _get_environment(urlopen=None): |
| 962 + """Detect the environment the code is being run on. |
| 963 + |
| 964 + Args: |
| 965 + urlopen: Optional argument. Function used to open a connection to a URL. |
| 966 + |
| 967 + Returns: |
| 968 + The value of SETTINGS.env_name after being set. If already |
| 969 + set, simply returns the value. |
| 970 + """ |
| 971 + if SETTINGS.env_name is not None: |
| 972 + return SETTINGS.env_name |
| 973 + |
| 974 + # None is an unset value, not the default. |
| 975 + SETTINGS.env_name = DEFAULT_ENV_NAME |
| 976 + |
| 977 + server_software = os.environ.get('SERVER_SOFTWARE', '') |
| 978 + if server_software.startswith('Google App Engine/'): |
| 979 + SETTINGS.env_name = 'GAE_PRODUCTION' |
| 980 + elif server_software.startswith('Development/'): |
| 981 + SETTINGS.env_name = 'GAE_LOCAL' |
| 982 + elif NO_GCE_CHECK != 'True' and _detect_gce_environment(urlopen=urlopen): |
| 983 + SETTINGS.env_name = 'GCE_PRODUCTION' |
| 984 + |
| 985 + return SETTINGS.env_name |
| 986 + |
| 987 + |
| 988 +class GoogleCredentials(OAuth2Credentials): |
| 989 + """Application Default Credentials for use in calling Google APIs. |
| 990 + |
| 991 + The Application Default Credentials are being constructed as a function of |
| 992 + the environment where the code is being run. |
| 993 + More details can be found on this page: |
| 994 + https://developers.google.com/accounts/docs/application-default-credentials |
| 995 + |
| 996 + Here is an example of how to use the Application Default Credentials for a |
| 997 + service that requires authentication: |
| 998 + |
| 999 + from googleapiclient.discovery import build |
| 1000 + from oauth2client.client import GoogleCredentials |
| 1001 + |
| 1002 + credentials = GoogleCredentials.get_application_default() |
| 1003 + service = build('compute', 'v1', credentials=credentials) |
| 1004 + |
| 1005 + PROJECT = 'bamboo-machine-422' |
| 1006 + ZONE = 'us-central1-a' |
| 1007 + request = service.instances().list(project=PROJECT, zone=ZONE) |
| 1008 + response = request.execute() |
| 1009 + |
| 1010 + print(response) |
| 1011 + """ |
| 1012 + |
| 1013 + def __init__(self, access_token, client_id, client_secret, refresh_token, |
| 1014 + token_expiry, token_uri, user_agent, |
| 1015 + revoke_uri=GOOGLE_REVOKE_URI): |
| 1016 + """Create an instance of GoogleCredentials. |
| 1017 + |
| 1018 + This constructor is not usually called by the user, instead |
| 1019 + GoogleCredentials objects are instantiated by |
| 1020 + GoogleCredentials.from_stream() or |
| 1021 + GoogleCredentials.get_application_default(). |
| 1022 + |
| 1023 + Args: |
| 1024 + access_token: string, access token. |
| 1025 + client_id: string, client identifier. |
| 1026 + client_secret: string, client secret. |
| 1027 + refresh_token: string, refresh token. |
| 1028 + token_expiry: datetime, when the access_token expires. |
| 1029 + token_uri: string, URI of token endpoint. |
| 1030 + user_agent: string, The HTTP User-Agent to provide for this application. |
| 1031 + revoke_uri: string, URI for revoke endpoint. |
| 1032 + Defaults to GOOGLE_REVOKE_URI; a token can't be revoked if this is None
. |
| 1033 + """ |
| 1034 + super(GoogleCredentials, self).__init__( |
| 1035 + access_token, client_id, client_secret, refresh_token, token_expiry, |
| 1036 + token_uri, user_agent, revoke_uri=revoke_uri) |
| 1037 + |
| 1038 + def create_scoped_required(self): |
| 1039 + """Whether this Credentials object is scopeless. |
| 1040 + |
| 1041 + create_scoped(scopes) method needs to be called in order to create |
| 1042 + a Credentials object for API calls. |
| 1043 + """ |
| 1044 + return False |
| 1045 + |
| 1046 + def create_scoped(self, scopes): |
| 1047 + """Create a Credentials object for the given scopes. |
| 1048 + |
| 1049 + The Credentials type is preserved. |
| 1050 + """ |
| 1051 + return self |
| 1052 + |
| 1053 + @property |
| 1054 + def serialization_data(self): |
| 1055 + """Get the fields and their values identifying the current credentials.""" |
| 1056 + return { |
| 1057 + 'type': 'authorized_user', |
| 1058 + 'client_id': self.client_id, |
| 1059 + 'client_secret': self.client_secret, |
| 1060 + 'refresh_token': self.refresh_token |
| 1061 + } |
| 1062 + |
| 1063 + @staticmethod |
| 1064 + def _implicit_credentials_from_gae(env_name=None): |
| 1065 + """Attempts to get implicit credentials in Google App Engine env. |
| 1066 + |
| 1067 + If the current environment is not detected as App Engine, returns None, |
| 1068 + indicating no Google App Engine credentials can be detected from the |
| 1069 + current environment. |
| 1070 + |
| 1071 + Args: |
| 1072 + env_name: String, indicating current environment. |
| 1073 + |
| 1074 + Returns: |
| 1075 + None, if not in GAE, else an appengine.AppAssertionCredentials object. |
| 1076 + """ |
| 1077 + env_name = env_name or _get_environment() |
| 1078 + if env_name not in ('GAE_PRODUCTION', 'GAE_LOCAL'): |
| 1079 + return None |
| 1080 + |
| 1081 + return _get_application_default_credential_GAE() |
| 1082 + |
| 1083 + @staticmethod |
| 1084 + def _implicit_credentials_from_gce(env_name=None): |
| 1085 + """Attempts to get implicit credentials in Google Compute Engine env. |
| 1086 + |
| 1087 + If the current environment is not detected as Compute Engine, returns None, |
| 1088 + indicating no Google Compute Engine credentials can be detected from the |
| 1089 + current environment. |
| 1090 + |
| 1091 + Args: |
| 1092 + env_name: String, indicating current environment. |
| 1093 + |
| 1094 + Returns: |
| 1095 + None, if not in GCE, else a gce.AppAssertionCredentials object. |
| 1096 + """ |
| 1097 + env_name = env_name or _get_environment() |
| 1098 + if env_name != 'GCE_PRODUCTION': |
| 1099 + return None |
| 1100 + |
| 1101 + return _get_application_default_credential_GCE() |
| 1102 + |
| 1103 + @staticmethod |
| 1104 + def _implicit_credentials_from_files(env_name=None): |
| 1105 + """Attempts to get implicit credentials from local credential files. |
| 1106 + |
| 1107 + First checks if the environment variable GOOGLE_APPLICATION_CREDENTIALS |
| 1108 + is set with a filename and then falls back to a configuration file (the |
| 1109 + "well known" file) associated with the 'gcloud' command line tool. |
| 1110 + |
| 1111 + Args: |
| 1112 + env_name: Unused argument. |
| 1113 + |
| 1114 + Returns: |
| 1115 + Credentials object associated with the GOOGLE_APPLICATION_CREDENTIALS |
| 1116 + file or the "well known" file if either exist. If neither file is |
| 1117 + define, returns None, indicating no credentials from a file can |
| 1118 + detected from the current environment. |
| 1119 + """ |
| 1120 + credentials_filename = _get_environment_variable_file() |
| 1121 + if not credentials_filename: |
| 1122 + credentials_filename = _get_well_known_file() |
| 1123 + if os.path.isfile(credentials_filename): |
| 1124 + extra_help = (' (produced automatically when running' |
| 1125 + ' "gcloud auth login" command)') |
| 1126 + else: |
| 1127 + credentials_filename = None |
| 1128 + else: |
| 1129 + extra_help = (' (pointed to by ' + GOOGLE_APPLICATION_CREDENTIALS + |
| 1130 + ' environment variable)') |
| 1131 + |
| 1132 + if not credentials_filename: |
| 1133 + return |
| 1134 + |
| 1135 + try: |
| 1136 + return _get_application_default_credential_from_file(credentials_filename
) |
| 1137 + except (ApplicationDefaultCredentialsError, ValueError) as error: |
| 1138 + _raise_exception_for_reading_json(credentials_filename, extra_help, error
) |
| 1139 + |
| 1140 + @classmethod |
| 1141 + def _get_implicit_credentials(cls): |
| 1142 + """Gets credentials implicitly from the environment. |
| 1143 + |
| 1144 + Checks environment in order of precedence: |
| 1145 + - Google App Engine (production and testing) |
| 1146 + - Environment variable GOOGLE_APPLICATION_CREDENTIALS pointing to |
| 1147 + a file with stored credentials information. |
| 1148 + - Stored "well known" file associated with `gcloud` command line tool. |
| 1149 + - Google Compute Engine production environment. |
| 1150 + |
| 1151 + Exceptions: |
| 1152 + ApplicationDefaultCredentialsError: raised when the credentials fail |
| 1153 + to be retrieved. |
| 1154 + """ |
| 1155 + env_name = _get_environment() |
| 1156 + |
| 1157 + # Environ checks (in order). Assumes each checker takes `env_name` |
| 1158 + # as a kwarg. |
| 1159 + environ_checkers = [ |
| 1160 + cls._implicit_credentials_from_gae, |
| 1161 + cls._implicit_credentials_from_files, |
| 1162 + cls._implicit_credentials_from_gce, |
| 1163 + ] |
| 1164 + |
| 1165 + for checker in environ_checkers: |
| 1166 + credentials = checker(env_name=env_name) |
| 1167 + if credentials is not None: |
| 1168 + return credentials |
| 1169 + |
| 1170 + # If no credentials, fail. |
| 1171 + raise ApplicationDefaultCredentialsError(ADC_HELP_MSG) |
| 1172 + |
| 1173 + @staticmethod |
| 1174 + def get_application_default(): |
| 1175 + """Get the Application Default Credentials for the current environment. |
| 1176 + |
| 1177 + Exceptions: |
| 1178 + ApplicationDefaultCredentialsError: raised when the credentials fail |
| 1179 + to be retrieved. |
| 1180 + """ |
| 1181 + return GoogleCredentials._get_implicit_credentials() |
| 1182 + |
| 1183 + @staticmethod |
| 1184 + def from_stream(credential_filename): |
| 1185 + """Create a Credentials object by reading the information from a given file
. |
| 1186 + |
| 1187 + It returns an object of type GoogleCredentials. |
| 1188 + |
| 1189 + Args: |
| 1190 + credential_filename: the path to the file from where the credentials |
| 1191 + are to be read |
| 1192 + |
| 1193 + Exceptions: |
| 1194 + ApplicationDefaultCredentialsError: raised when the credentials fail |
| 1195 + to be retrieved. |
| 1196 + """ |
| 1197 + |
| 1198 + if credential_filename and os.path.isfile(credential_filename): |
| 1199 + try: |
| 1200 + return _get_application_default_credential_from_file( |
| 1201 + credential_filename) |
| 1202 + except (ApplicationDefaultCredentialsError, ValueError) as error: |
| 1203 + extra_help = ' (provided as parameter to the from_stream() method)' |
| 1204 + _raise_exception_for_reading_json(credential_filename, |
| 1205 + extra_help, |
| 1206 + error) |
| 1207 + else: |
| 1208 + raise ApplicationDefaultCredentialsError( |
| 1209 + 'The parameter passed to the from_stream() ' |
| 1210 + 'method should point to a file.') |
| 1211 + |
| 1212 + |
| 1213 +def _save_private_file(filename, json_contents): |
| 1214 + """Saves a file with read-write permissions on for the owner. |
| 1215 + |
| 1216 + Args: |
| 1217 + filename: String. Absolute path to file. |
| 1218 + json_contents: JSON serializable object to be saved. |
| 1219 + """ |
| 1220 + temp_filename = tempfile.mktemp() |
| 1221 + file_desc = os.open(temp_filename, os.O_WRONLY | os.O_CREAT, 0o600) |
| 1222 + with os.fdopen(file_desc, 'w') as file_handle: |
| 1223 + json.dump(json_contents, file_handle, sort_keys=True, |
| 1224 + indent=2, separators=(',', ': ')) |
| 1225 + shutil.move(temp_filename, filename) |
| 1226 + |
| 1227 + |
| 1228 +def save_to_well_known_file(credentials, well_known_file=None): |
| 1229 + """Save the provided GoogleCredentials to the well known file. |
| 1230 + |
| 1231 + Args: |
| 1232 + credentials: |
| 1233 + the credentials to be saved to the well known file; |
| 1234 + it should be an instance of GoogleCredentials |
| 1235 + well_known_file: |
| 1236 + the name of the file where the credentials are to be saved; |
| 1237 + this parameter is supposed to be used for testing only |
| 1238 + """ |
| 1239 + # TODO(orestica): move this method to tools.py |
| 1240 + # once the argparse import gets fixed (it is not present in Python 2.6) |
| 1241 + |
| 1242 + if well_known_file is None: |
| 1243 + well_known_file = _get_well_known_file() |
| 1244 + |
| 1245 + credentials_data = credentials.serialization_data |
| 1246 + _save_private_file(well_known_file, credentials_data) |
| 1247 + |
| 1248 + |
| 1249 +def _get_environment_variable_file(): |
| 1250 + application_default_credential_filename = ( |
| 1251 + os.environ.get(GOOGLE_APPLICATION_CREDENTIALS, |
| 1252 + None)) |
| 1253 + |
| 1254 + if application_default_credential_filename: |
| 1255 + if os.path.isfile(application_default_credential_filename): |
| 1256 + return application_default_credential_filename |
| 1257 + else: |
| 1258 + raise ApplicationDefaultCredentialsError( |
| 1259 + 'File ' + application_default_credential_filename + ' (pointed by ' + |
| 1260 + GOOGLE_APPLICATION_CREDENTIALS + |
| 1261 + ' environment variable) does not exist!') |
| 1262 + |
| 1263 + |
| 1264 +def _get_well_known_file(): |
| 1265 + """Get the well known file produced by command 'gcloud auth login'.""" |
| 1266 + # TODO(orestica): Revisit this method once gcloud provides a better way |
| 1267 + # of pinpointing the exact location of the file. |
| 1268 + |
| 1269 + WELL_KNOWN_CREDENTIALS_FILE = 'application_default_credentials.json' |
| 1270 + |
| 1271 + if os.name == 'nt': |
| 1272 + try: |
| 1273 + default_config_path = os.path.join(os.environ['APPDATA'], |
| 1274 + _CLOUDSDK_CONFIG_DIRECTORY) |
| 1275 + except KeyError: |
| 1276 + # This should never happen unless someone is really messing with things. |
| 1277 + drive = os.environ.get('SystemDrive', 'C:') |
| 1278 + default_config_path = os.path.join(drive, '\\', |
| 1279 + _CLOUDSDK_CONFIG_DIRECTORY) |
| 1280 + else: |
| 1281 + default_config_path = os.path.join(os.path.expanduser('~'), |
| 1282 + '.config', |
| 1283 + _CLOUDSDK_CONFIG_DIRECTORY) |
| 1284 + |
| 1285 + default_config_path = os.path.join(default_config_path, |
| 1286 + WELL_KNOWN_CREDENTIALS_FILE) |
| 1287 + |
| 1288 + return default_config_path |
| 1289 + |
| 1290 + |
| 1291 +def _get_application_default_credential_from_file(filename): |
| 1292 + """Build the Application Default Credentials from file.""" |
| 1293 + |
| 1294 + from oauth2client import service_account |
| 1295 + |
| 1296 + # read the credentials from the file |
| 1297 + with open(filename) as file_obj: |
| 1298 + client_credentials = json.load(file_obj) |
| 1299 + |
| 1300 + credentials_type = client_credentials.get('type') |
| 1301 + if credentials_type == AUTHORIZED_USER: |
| 1302 + required_fields = set(['client_id', 'client_secret', 'refresh_token']) |
| 1303 + elif credentials_type == SERVICE_ACCOUNT: |
| 1304 + required_fields = set(['client_id', 'client_email', 'private_key_id', |
| 1305 + 'private_key']) |
| 1306 + else: |
| 1307 + raise ApplicationDefaultCredentialsError( |
| 1308 + "'type' field should be defined (and have one of the '" + |
| 1309 + AUTHORIZED_USER + "' or '" + SERVICE_ACCOUNT + "' values)") |
| 1310 + |
| 1311 + missing_fields = required_fields.difference(client_credentials.keys()) |
| 1312 + |
| 1313 + if missing_fields: |
| 1314 + _raise_exception_for_missing_fields(missing_fields) |
| 1315 + |
| 1316 + if client_credentials['type'] == AUTHORIZED_USER: |
| 1317 + return GoogleCredentials( |
| 1318 + access_token=None, |
| 1319 + client_id=client_credentials['client_id'], |
| 1320 + client_secret=client_credentials['client_secret'], |
| 1321 + refresh_token=client_credentials['refresh_token'], |
| 1322 + token_expiry=None, |
| 1323 + token_uri=GOOGLE_TOKEN_URI, |
| 1324 + user_agent='Python client library') |
| 1325 + else: # client_credentials['type'] == SERVICE_ACCOUNT |
| 1326 + return service_account._ServiceAccountCredentials( |
| 1327 + service_account_id=client_credentials['client_id'], |
| 1328 + service_account_email=client_credentials['client_email'], |
| 1329 + private_key_id=client_credentials['private_key_id'], |
| 1330 + private_key_pkcs8_text=client_credentials['private_key'], |
| 1331 + scopes=[]) |
| 1332 + |
| 1333 + |
| 1334 +def _raise_exception_for_missing_fields(missing_fields): |
| 1335 + raise ApplicationDefaultCredentialsError( |
| 1336 + 'The following field(s) must be defined: ' + ', '.join(missing_fields)) |
| 1337 + |
| 1338 + |
| 1339 +def _raise_exception_for_reading_json(credential_file, |
| 1340 + extra_help, |
| 1341 + error): |
| 1342 + raise ApplicationDefaultCredentialsError( |
| 1343 + 'An error was encountered while reading json file: '+ |
| 1344 + credential_file + extra_help + ': ' + str(error)) |
| 1345 + |
| 1346 + |
| 1347 +def _get_application_default_credential_GAE(): |
| 1348 + from oauth2client.appengine import AppAssertionCredentials |
| 1349 + |
| 1350 + return AppAssertionCredentials([]) |
| 1351 + |
| 1352 + |
| 1353 +def _get_application_default_credential_GCE(): |
| 1354 + from oauth2client.gce import AppAssertionCredentials |
| 1355 + |
| 1356 + return AppAssertionCredentials([]) |
| 1357 + |
| 1358 + |
| 1359 +class AssertionCredentials(GoogleCredentials): |
| 1360 """Abstract Credentials object used for OAuth 2.0 assertion grants. |
| 1361 |
| 1362 This credential does not require a flow to instantiate because it |
| 1363 @@ -859,7 +1396,7 @@ class AssertionCredentials(OAuth2Credentials): |
| 1364 def _generate_refresh_request_body(self): |
| 1365 assertion = self._generate_assertion() |
| 1366 |
| 1367 - body = urllib.urlencode({ |
| 1368 + body = urllib.parse.urlencode({ |
| 1369 'assertion': assertion, |
| 1370 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', |
| 1371 }) |
| 1372 @@ -882,141 +1419,157 @@ class AssertionCredentials(OAuth2Credentials): |
| 1373 self._do_revoke(http_request, self.access_token) |
| 1374 |
| 1375 |
| 1376 -if HAS_CRYPTO: |
| 1377 - # PyOpenSSL and PyCrypto are not prerequisites for oauth2client, so if it is |
| 1378 - # missing then don't create the SignedJwtAssertionCredentials or the |
| 1379 - # verify_id_token() method. |
| 1380 +def _RequireCryptoOrDie(): |
| 1381 + """Ensure we have a crypto library, or throw CryptoUnavailableError. |
| 1382 + |
| 1383 + The oauth2client.crypt module requires either PyCrypto or PyOpenSSL |
| 1384 + to be available in order to function, but these are optional |
| 1385 + dependencies. |
| 1386 + """ |
| 1387 + if not HAS_CRYPTO: |
| 1388 + raise CryptoUnavailableError('No crypto library available') |
| 1389 |
| 1390 - class SignedJwtAssertionCredentials(AssertionCredentials): |
| 1391 - """Credentials object used for OAuth 2.0 Signed JWT assertion grants. |
| 1392 |
| 1393 - This credential does not require a flow to instantiate because it represent
s |
| 1394 - a two legged flow, and therefore has all of the required information to |
| 1395 - generate and refresh its own access tokens. |
| 1396 +class SignedJwtAssertionCredentials(AssertionCredentials): |
| 1397 + """Credentials object used for OAuth 2.0 Signed JWT assertion grants. |
| 1398 + |
| 1399 + This credential does not require a flow to instantiate because it |
| 1400 + represents a two legged flow, and therefore has all of the required |
| 1401 + information to generate and refresh its own access tokens. |
| 1402 + |
| 1403 + SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto |
| 1404 + 2.6 or later. For App Engine you may also consider using |
| 1405 + AppAssertionCredentials. |
| 1406 + """ |
| 1407 + |
| 1408 + MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds |
| 1409 + |
| 1410 + @util.positional(4) |
| 1411 + def __init__(self, |
| 1412 + service_account_name, |
| 1413 + private_key, |
| 1414 + scope, |
| 1415 + private_key_password='notasecret', |
| 1416 + user_agent=None, |
| 1417 + token_uri=GOOGLE_TOKEN_URI, |
| 1418 + revoke_uri=GOOGLE_REVOKE_URI, |
| 1419 + **kwargs): |
| 1420 + """Constructor for SignedJwtAssertionCredentials. |
| 1421 + |
| 1422 + Args: |
| 1423 + service_account_name: string, id for account, usually an email address. |
| 1424 + private_key: string, private key in PKCS12 or PEM format. |
| 1425 + scope: string or iterable of strings, scope(s) of the credentials being |
| 1426 + requested. |
| 1427 + private_key_password: string, password for private_key, unused if |
| 1428 + private_key is in PEM format. |
| 1429 + user_agent: string, HTTP User-Agent to provide for this application. |
| 1430 + token_uri: string, URI for token endpoint. For convenience |
| 1431 + defaults to Google's endpoints but any OAuth 2.0 provider can be used. |
| 1432 + revoke_uri: string, URI for revoke endpoint. |
| 1433 + kwargs: kwargs, Additional parameters to add to the JWT token, for |
| 1434 + example sub=joe@xample.org. |
| 1435 |
| 1436 - SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto 2.6 or |
| 1437 - later. For App Engine you may also consider using AppAssertionCredentials. |
| 1438 + Raises: |
| 1439 + CryptoUnavailableError if no crypto library is available. |
| 1440 """ |
| 1441 + _RequireCryptoOrDie() |
| 1442 + super(SignedJwtAssertionCredentials, self).__init__( |
| 1443 + None, |
| 1444 + user_agent=user_agent, |
| 1445 + token_uri=token_uri, |
| 1446 + revoke_uri=revoke_uri, |
| 1447 + ) |
| 1448 |
| 1449 - MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds |
| 1450 + self.scope = util.scopes_to_string(scope) |
| 1451 |
| 1452 - @util.positional(4) |
| 1453 - def __init__(self, |
| 1454 - service_account_name, |
| 1455 - private_key, |
| 1456 - scope, |
| 1457 - private_key_password='notasecret', |
| 1458 - user_agent=None, |
| 1459 - token_uri=GOOGLE_TOKEN_URI, |
| 1460 - revoke_uri=GOOGLE_REVOKE_URI, |
| 1461 - **kwargs): |
| 1462 - """Constructor for SignedJwtAssertionCredentials. |
| 1463 - |
| 1464 - Args: |
| 1465 - service_account_name: string, id for account, usually an email address. |
| 1466 - private_key: string, private key in PKCS12 or PEM format. |
| 1467 - scope: string or iterable of strings, scope(s) of the credentials being |
| 1468 - requested. |
| 1469 - private_key_password: string, password for private_key, unused if |
| 1470 - private_key is in PEM format. |
| 1471 - user_agent: string, HTTP User-Agent to provide for this application. |
| 1472 - token_uri: string, URI for token endpoint. For convenience |
| 1473 - defaults to Google's endpoints but any OAuth 2.0 provider can be used
. |
| 1474 - revoke_uri: string, URI for revoke endpoint. |
| 1475 - kwargs: kwargs, Additional parameters to add to the JWT token, for |
| 1476 - example sub=joe@xample.org.""" |
| 1477 - |
| 1478 - super(SignedJwtAssertionCredentials, self).__init__( |
| 1479 - None, |
| 1480 - user_agent=user_agent, |
| 1481 - token_uri=token_uri, |
| 1482 - revoke_uri=revoke_uri, |
| 1483 - ) |
| 1484 - |
| 1485 - self.scope = util.scopes_to_string(scope) |
| 1486 - |
| 1487 - # Keep base64 encoded so it can be stored in JSON. |
| 1488 - self.private_key = base64.b64encode(private_key) |
| 1489 - |
| 1490 - self.private_key_password = private_key_password |
| 1491 - self.service_account_name = service_account_name |
| 1492 - self.kwargs = kwargs |
| 1493 - |
| 1494 - @classmethod |
| 1495 - def from_json(cls, s): |
| 1496 - data = simplejson.loads(s) |
| 1497 - retval = SignedJwtAssertionCredentials( |
| 1498 - data['service_account_name'], |
| 1499 - base64.b64decode(data['private_key']), |
| 1500 - data['scope'], |
| 1501 - private_key_password=data['private_key_password'], |
| 1502 - user_agent=data['user_agent'], |
| 1503 - token_uri=data['token_uri'], |
| 1504 - **data['kwargs'] |
| 1505 - ) |
| 1506 - retval.invalid = data['invalid'] |
| 1507 - retval.access_token = data['access_token'] |
| 1508 - return retval |
| 1509 - |
| 1510 - def _generate_assertion(self): |
| 1511 - """Generate the assertion that will be used in the request.""" |
| 1512 - now = long(time.time()) |
| 1513 - payload = { |
| 1514 - 'aud': self.token_uri, |
| 1515 - 'scope': self.scope, |
| 1516 - 'iat': now, |
| 1517 - 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS, |
| 1518 - 'iss': self.service_account_name |
| 1519 - } |
| 1520 - payload.update(self.kwargs) |
| 1521 - logger.debug(str(payload)) |
| 1522 + # Keep base64 encoded so it can be stored in JSON. |
| 1523 + self.private_key = base64.b64encode(private_key) |
| 1524 + if isinstance(self.private_key, six.text_type): |
| 1525 + self.private_key = self.private_key.encode('utf-8') |
| 1526 |
| 1527 - private_key = base64.b64decode(self.private_key) |
| 1528 - return crypt.make_signed_jwt(crypt.Signer.from_string( |
| 1529 - private_key, self.private_key_password), payload) |
| 1530 + self.private_key_password = private_key_password |
| 1531 + self.service_account_name = service_account_name |
| 1532 + self.kwargs = kwargs |
| 1533 |
| 1534 - # Only used in verify_id_token(), which is always calling to the same URI |
| 1535 - # for the certs. |
| 1536 - _cached_http = httplib2.Http(MemoryCache()) |
| 1537 + @classmethod |
| 1538 + def from_json(cls, s): |
| 1539 + data = json.loads(s) |
| 1540 + retval = SignedJwtAssertionCredentials( |
| 1541 + data['service_account_name'], |
| 1542 + base64.b64decode(data['private_key']), |
| 1543 + data['scope'], |
| 1544 + private_key_password=data['private_key_password'], |
| 1545 + user_agent=data['user_agent'], |
| 1546 + token_uri=data['token_uri'], |
| 1547 + **data['kwargs'] |
| 1548 + ) |
| 1549 + retval.invalid = data['invalid'] |
| 1550 + retval.access_token = data['access_token'] |
| 1551 + return retval |
| 1552 |
| 1553 - @util.positional(2) |
| 1554 - def verify_id_token(id_token, audience, http=None, |
| 1555 - cert_uri=ID_TOKEN_VERIFICATON_CERTS): |
| 1556 - """Verifies a signed JWT id_token. |
| 1557 + def _generate_assertion(self): |
| 1558 + """Generate the assertion that will be used in the request.""" |
| 1559 + now = int(time.time()) |
| 1560 + payload = { |
| 1561 + 'aud': self.token_uri, |
| 1562 + 'scope': self.scope, |
| 1563 + 'iat': now, |
| 1564 + 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS, |
| 1565 + 'iss': self.service_account_name |
| 1566 + } |
| 1567 + payload.update(self.kwargs) |
| 1568 + logger.debug(str(payload)) |
| 1569 |
| 1570 - This function requires PyOpenSSL and because of that it does not work on |
| 1571 - App Engine. |
| 1572 + private_key = base64.b64decode(self.private_key) |
| 1573 + return crypt.make_signed_jwt(crypt.Signer.from_string( |
| 1574 + private_key, self.private_key_password), payload) |
| 1575 |
| 1576 - Args: |
| 1577 - id_token: string, A Signed JWT. |
| 1578 - audience: string, The audience 'aud' that the token should be for. |
| 1579 - http: httplib2.Http, instance to use to make the HTTP request. Callers |
| 1580 - should supply an instance that has caching enabled. |
| 1581 - cert_uri: string, URI of the certificates in JSON format to |
| 1582 - verify the JWT against. |
| 1583 +# Only used in verify_id_token(), which is always calling to the same URI |
| 1584 +# for the certs. |
| 1585 +_cached_http = httplib2.Http(MemoryCache()) |
| 1586 |
| 1587 - Returns: |
| 1588 - The deserialized JSON in the JWT. |
| 1589 +@util.positional(2) |
| 1590 +def verify_id_token(id_token, audience, http=None, |
| 1591 + cert_uri=ID_TOKEN_VERIFICATION_CERTS): |
| 1592 + """Verifies a signed JWT id_token. |
| 1593 |
| 1594 - Raises: |
| 1595 - oauth2client.crypt.AppIdentityError if the JWT fails to verify. |
| 1596 - """ |
| 1597 - if http is None: |
| 1598 - http = _cached_http |
| 1599 + This function requires PyOpenSSL and because of that it does not work on |
| 1600 + App Engine. |
| 1601 |
| 1602 - resp, content = http.request(cert_uri) |
| 1603 + Args: |
| 1604 + id_token: string, A Signed JWT. |
| 1605 + audience: string, The audience 'aud' that the token should be for. |
| 1606 + http: httplib2.Http, instance to use to make the HTTP request. Callers |
| 1607 + should supply an instance that has caching enabled. |
| 1608 + cert_uri: string, URI of the certificates in JSON format to |
| 1609 + verify the JWT against. |
| 1610 |
| 1611 - if resp.status == 200: |
| 1612 - certs = simplejson.loads(content) |
| 1613 - return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) |
| 1614 - else: |
| 1615 - raise VerifyJwtTokenError('Status code: %d' % resp.status) |
| 1616 + Returns: |
| 1617 + The deserialized JSON in the JWT. |
| 1618 + |
| 1619 + Raises: |
| 1620 + oauth2client.crypt.AppIdentityError: if the JWT fails to verify. |
| 1621 + CryptoUnavailableError: if no crypto library is available. |
| 1622 + """ |
| 1623 + _RequireCryptoOrDie() |
| 1624 + if http is None: |
| 1625 + http = _cached_http |
| 1626 + |
| 1627 + resp, content = http.request(cert_uri) |
| 1628 + |
| 1629 + if resp.status == 200: |
| 1630 + certs = json.loads(content.decode('utf-8')) |
| 1631 + return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) |
| 1632 + else: |
| 1633 + raise VerifyJwtTokenError('Status code: %d' % resp.status) |
| 1634 |
| 1635 |
| 1636 def _urlsafe_b64decode(b64string): |
| 1637 # Guard against unicode strings, which base64 can't handle. |
| 1638 - b64string = b64string.encode('ascii') |
| 1639 - padded = b64string + '=' * (4 - len(b64string) % 4) |
| 1640 + if isinstance(b64string, six.text_type): |
| 1641 + b64string = b64string.encode('ascii') |
| 1642 + padded = b64string + b'=' * (4 - len(b64string) % 4) |
| 1643 return base64.urlsafe_b64decode(padded) |
| 1644 |
| 1645 |
| 1646 @@ -1026,18 +1579,21 @@ def _extract_id_token(id_token): |
| 1647 Does the extraction w/o checking the signature. |
| 1648 |
| 1649 Args: |
| 1650 - id_token: string, OAuth 2.0 id_token. |
| 1651 + id_token: string or bytestring, OAuth 2.0 id_token. |
| 1652 |
| 1653 Returns: |
| 1654 object, The deserialized JSON payload. |
| 1655 """ |
| 1656 - segments = id_token.split('.') |
| 1657 + if type(id_token) == bytes: |
| 1658 + segments = id_token.split(b'.') |
| 1659 + else: |
| 1660 + segments = id_token.split(u'.') |
| 1661 |
| 1662 - if (len(segments) != 3): |
| 1663 + if len(segments) != 3: |
| 1664 raise VerifyJwtTokenError( |
| 1665 - 'Wrong number of segments in token: %s' % id_token) |
| 1666 + 'Wrong number of segments in token: %s' % id_token) |
| 1667 |
| 1668 - return simplejson.loads(_urlsafe_b64decode(segments[1])) |
| 1669 + return json.loads(_urlsafe_b64decode(segments[1]).decode('utf-8')) |
| 1670 |
| 1671 |
| 1672 def _parse_exchange_token_response(content): |
| 1673 @@ -1055,11 +1611,12 @@ def _parse_exchange_token_response(content): |
| 1674 """ |
| 1675 resp = {} |
| 1676 try: |
| 1677 - resp = simplejson.loads(content) |
| 1678 - except StandardError: |
| 1679 + resp = json.loads(content.decode('utf-8')) |
| 1680 + except Exception: |
| 1681 # different JSON libs raise different exceptions, |
| 1682 # so we just do a catch-all here |
| 1683 - resp = dict(parse_qsl(content)) |
| 1684 + content = content.decode('utf-8') |
| 1685 + resp = dict(urllib.parse.parse_qsl(content)) |
| 1686 |
| 1687 # some providers respond with 'expires', others with 'expires_in' |
| 1688 if resp and 'expires' in resp: |
| 1689 @@ -1073,14 +1630,15 @@ def credentials_from_code(client_id, client_secret, scop
e, code, |
| 1690 redirect_uri='postmessage', http=None, |
| 1691 user_agent=None, token_uri=GOOGLE_TOKEN_URI, |
| 1692 auth_uri=GOOGLE_AUTH_URI, |
| 1693 - revoke_uri=GOOGLE_REVOKE_URI): |
| 1694 + revoke_uri=GOOGLE_REVOKE_URI, |
| 1695 + device_uri=GOOGLE_DEVICE_URI): |
| 1696 """Exchanges an authorization code for an OAuth2Credentials object. |
| 1697 |
| 1698 Args: |
| 1699 client_id: string, client identifier. |
| 1700 client_secret: string, client secret. |
| 1701 scope: string or iterable of strings, scope(s) to request. |
| 1702 - code: string, An authroization code, most likely passed down from |
| 1703 + code: string, An authorization code, most likely passed down from |
| 1704 the client |
| 1705 redirect_uri: string, this is generally set to 'postmessage' to match the |
| 1706 redirect_uri that the client specified |
| 1707 @@ -1091,6 +1649,8 @@ def credentials_from_code(client_id, client_secret, scope,
code, |
| 1708 defaults to Google's endpoints but any OAuth 2.0 provider can be used. |
| 1709 revoke_uri: string, URI for revoke endpoint. For convenience |
| 1710 defaults to Google's endpoints but any OAuth 2.0 provider can be used. |
| 1711 + device_uri: string, URI for device authorization endpoint. For convenience |
| 1712 + defaults to Google's endpoints but any OAuth 2.0 provider can be used. |
| 1713 |
| 1714 Returns: |
| 1715 An OAuth2Credentials object. |
| 1716 @@ -1102,7 +1662,7 @@ def credentials_from_code(client_id, client_secret, scope,
code, |
| 1717 flow = OAuth2WebServerFlow(client_id, client_secret, scope, |
| 1718 redirect_uri=redirect_uri, user_agent=user_agent, |
| 1719 auth_uri=auth_uri, token_uri=token_uri, |
| 1720 - revoke_uri=revoke_uri) |
| 1721 + revoke_uri=revoke_uri, device_uri=device_uri) |
| 1722 |
| 1723 credentials = flow.step2_exchange(code, http=http) |
| 1724 return credentials |
| 1725 @@ -1113,7 +1673,8 @@ def credentials_from_clientsecrets_and_code(filename, scop
e, code, |
| 1726 message = None, |
| 1727 redirect_uri='postmessage', |
| 1728 http=None, |
| 1729 - cache=None): |
| 1730 + cache=None, |
| 1731 + device_uri=None): |
| 1732 """Returns OAuth2Credentials from a clientsecrets file and an auth code. |
| 1733 |
| 1734 Will create the right kind of Flow based on the contents of the clientsecrets |
| 1735 @@ -1133,6 +1694,7 @@ def credentials_from_clientsecrets_and_code(filename, scop
e, code, |
| 1736 http: httplib2.Http, optional http instance to use to do the fetch |
| 1737 cache: An optional cache service client that implements get() and set() |
| 1738 methods. See clientsecrets.loadfile() for details. |
| 1739 + device_uri: string, OAuth 2.0 device authorization endpoint |
| 1740 |
| 1741 Returns: |
| 1742 An OAuth2Credentials object. |
| 1743 @@ -1145,11 +1707,49 @@ def credentials_from_clientsecrets_and_code(filename, sc
ope, code, |
| 1744 invalid. |
| 1745 """ |
| 1746 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache, |
| 1747 - redirect_uri=redirect_uri) |
| 1748 + redirect_uri=redirect_uri, |
| 1749 + device_uri=device_uri) |
| 1750 credentials = flow.step2_exchange(code, http=http) |
| 1751 return credentials |
| 1752 |
| 1753 |
| 1754 +class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', ( |
| 1755 + 'device_code', 'user_code', 'interval', 'verification_url', |
| 1756 + 'user_code_expiry'))): |
| 1757 + """Intermediate information the OAuth2 for devices flow.""" |
| 1758 + |
| 1759 + @classmethod |
| 1760 + def FromResponse(cls, response): |
| 1761 + """Create a DeviceFlowInfo from a server response. |
| 1762 + |
| 1763 + The response should be a dict containing entries as described here: |
| 1764 + |
| 1765 + http://tools.ietf.org/html/draft-ietf-oauth-v2-05#section-3.7.1 |
| 1766 + """ |
| 1767 + # device_code, user_code, and verification_url are required. |
| 1768 + kwargs = { |
| 1769 + 'device_code': response['device_code'], |
| 1770 + 'user_code': response['user_code'], |
| 1771 + } |
| 1772 + # The response may list the verification address as either |
| 1773 + # verification_url or verification_uri, so we check for both. |
| 1774 + verification_url = response.get( |
| 1775 + 'verification_url', response.get('verification_uri')) |
| 1776 + if verification_url is None: |
| 1777 + raise OAuth2DeviceCodeError( |
| 1778 + 'No verification_url provided in server response') |
| 1779 + kwargs['verification_url'] = verification_url |
| 1780 + # expires_in and interval are optional. |
| 1781 + kwargs.update({ |
| 1782 + 'interval': response.get('interval'), |
| 1783 + 'user_code_expiry': None, |
| 1784 + }) |
| 1785 + if 'expires_in' in response: |
| 1786 + kwargs['user_code_expiry'] = datetime.datetime.now() + datetime.timedelta
( |
| 1787 + seconds=int(response['expires_in'])) |
| 1788 + |
| 1789 + return cls(**kwargs) |
| 1790 + |
| 1791 class OAuth2WebServerFlow(Flow): |
| 1792 """Does the Web Server Flow for OAuth 2.0. |
| 1793 |
| 1794 @@ -1163,6 +1763,8 @@ class OAuth2WebServerFlow(Flow): |
| 1795 auth_uri=GOOGLE_AUTH_URI, |
| 1796 token_uri=GOOGLE_TOKEN_URI, |
| 1797 revoke_uri=GOOGLE_REVOKE_URI, |
| 1798 + login_hint=None, |
| 1799 + device_uri=GOOGLE_DEVICE_URI, |
| 1800 **kwargs): |
| 1801 """Constructor for OAuth2WebServerFlow. |
| 1802 |
| 1803 @@ -1185,6 +1787,11 @@ class OAuth2WebServerFlow(Flow): |
| 1804 defaults to Google's endpoints but any OAuth 2.0 provider can be used. |
| 1805 revoke_uri: string, URI for revoke endpoint. For convenience |
| 1806 defaults to Google's endpoints but any OAuth 2.0 provider can be used. |
| 1807 + login_hint: string, Either an email address or domain. Passing this hint |
| 1808 + will either pre-fill the email box on the sign-in form or select the |
| 1809 + proper multi-login session, thereby simplifying the login flow. |
| 1810 + device_uri: string, URI for device authorization endpoint. For convenienc
e |
| 1811 + defaults to Google's endpoints but any OAuth 2.0 provider can be used. |
| 1812 **kwargs: dict, The keyword arguments are all optional and required |
| 1813 parameters for the OAuth calls. |
| 1814 """ |
| 1815 @@ -1192,10 +1799,12 @@ class OAuth2WebServerFlow(Flow): |
| 1816 self.client_secret = client_secret |
| 1817 self.scope = util.scopes_to_string(scope) |
| 1818 self.redirect_uri = redirect_uri |
| 1819 + self.login_hint = login_hint |
| 1820 self.user_agent = user_agent |
| 1821 self.auth_uri = auth_uri |
| 1822 self.token_uri = token_uri |
| 1823 self.revoke_uri = revoke_uri |
| 1824 + self.device_uri = device_uri |
| 1825 self.params = { |
| 1826 'access_type': 'offline', |
| 1827 'response_type': 'code', |
| 1828 @@ -1216,8 +1825,9 @@ class OAuth2WebServerFlow(Flow): |
| 1829 A URI as a string to redirect the user to begin the authorization flow. |
| 1830 """ |
| 1831 if redirect_uri is not None: |
| 1832 - logger.warning(('The redirect_uri parameter for' |
| 1833 - 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please' |
| 1834 + logger.warning(( |
| 1835 + 'The redirect_uri parameter for ' |
| 1836 + 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please ' |
| 1837 'move to passing the redirect_uri in via the constructor.')) |
| 1838 self.redirect_uri = redirect_uri |
| 1839 |
| 1840 @@ -1229,45 +1839,107 @@ class OAuth2WebServerFlow(Flow): |
| 1841 'redirect_uri': self.redirect_uri, |
| 1842 'scope': self.scope, |
| 1843 } |
| 1844 + if self.login_hint is not None: |
| 1845 + query_params['login_hint'] = self.login_hint |
| 1846 query_params.update(self.params) |
| 1847 return _update_query_params(self.auth_uri, query_params) |
| 1848 |
| 1849 + @util.positional(1) |
| 1850 + def step1_get_device_and_user_codes(self, http=None): |
| 1851 + """Returns a user code and the verification URL where to enter it |
| 1852 + |
| 1853 + Returns: |
| 1854 + A user code as a string for the user to authorize the application |
| 1855 + An URL as a string where the user has to enter the code |
| 1856 + """ |
| 1857 + if self.device_uri is None: |
| 1858 + raise ValueError('The value of device_uri must not be None.') |
| 1859 + |
| 1860 + body = urllib.parse.urlencode({ |
| 1861 + 'client_id': self.client_id, |
| 1862 + 'scope': self.scope, |
| 1863 + }) |
| 1864 + headers = { |
| 1865 + 'content-type': 'application/x-www-form-urlencoded', |
| 1866 + } |
| 1867 + |
| 1868 + if self.user_agent is not None: |
| 1869 + headers['user-agent'] = self.user_agent |
| 1870 + |
| 1871 + if http is None: |
| 1872 + http = httplib2.Http() |
| 1873 + |
| 1874 + resp, content = http.request(self.device_uri, method='POST', body=body, |
| 1875 + headers=headers) |
| 1876 + if resp.status == 200: |
| 1877 + try: |
| 1878 + flow_info = json.loads(content) |
| 1879 + except ValueError as e: |
| 1880 + raise OAuth2DeviceCodeError( |
| 1881 + 'Could not parse server response as JSON: "%s", error: "%s"' % ( |
| 1882 + content, e)) |
| 1883 + return DeviceFlowInfo.FromResponse(flow_info) |
| 1884 + else: |
| 1885 + error_msg = 'Invalid response %s.' % resp.status |
| 1886 + try: |
| 1887 + d = json.loads(content) |
| 1888 + if 'error' in d: |
| 1889 + error_msg += ' Error: %s' % d['error'] |
| 1890 + except ValueError: |
| 1891 + # Couldn't decode a JSON response, stick with the default message. |
| 1892 + pass |
| 1893 + raise OAuth2DeviceCodeError(error_msg) |
| 1894 + |
| 1895 @util.positional(2) |
| 1896 - def step2_exchange(self, code, http=None): |
| 1897 - """Exhanges a code for OAuth2Credentials. |
| 1898 + def step2_exchange(self, code=None, http=None, device_flow_info=None): |
| 1899 + """Exchanges a code for OAuth2Credentials. |
| 1900 |
| 1901 Args: |
| 1902 - code: string or dict, either the code as a string, or a dictionary |
| 1903 - of the query parameters to the redirect_uri, which contains |
| 1904 - the code. |
| 1905 - http: httplib2.Http, optional http instance to use to do the fetch |
| 1906 + |
| 1907 + code: string, a dict-like object, or None. For a non-device |
| 1908 + flow, this is either the response code as a string, or a |
| 1909 + dictionary of query parameters to the redirect_uri. For a |
| 1910 + device flow, this should be None. |
| 1911 + http: httplib2.Http, optional http instance to use when fetching |
| 1912 + credentials. |
| 1913 + device_flow_info: DeviceFlowInfo, return value from step1 in the |
| 1914 + case of a device flow. |
| 1915 |
| 1916 Returns: |
| 1917 An OAuth2Credentials object that can be used to authorize requests. |
| 1918 |
| 1919 Raises: |
| 1920 - FlowExchangeError if a problem occured exchanging the code for a |
| 1921 - refresh_token. |
| 1922 - """ |
| 1923 + FlowExchangeError: if a problem occurred exchanging the code for a |
| 1924 + refresh_token. |
| 1925 + ValueError: if code and device_flow_info are both provided or both |
| 1926 + missing. |
| 1927 |
| 1928 - if not (isinstance(code, str) or isinstance(code, unicode)): |
| 1929 + """ |
| 1930 + if code is None and device_flow_info is None: |
| 1931 + raise ValueError('No code or device_flow_info provided.') |
| 1932 + if code is not None and device_flow_info is not None: |
| 1933 + raise ValueError('Cannot provide both code and device_flow_info.') |
| 1934 + |
| 1935 + if code is None: |
| 1936 + code = device_flow_info.device_code |
| 1937 + elif not isinstance(code, six.string_types): |
| 1938 if 'code' not in code: |
| 1939 - if 'error' in code: |
| 1940 - error_msg = code['error'] |
| 1941 - else: |
| 1942 - error_msg = 'No code was supplied in the query parameters.' |
| 1943 - raise FlowExchangeError(error_msg) |
| 1944 - else: |
| 1945 - code = code['code'] |
| 1946 + raise FlowExchangeError(code.get( |
| 1947 + 'error', 'No code was supplied in the query parameters.')) |
| 1948 + code = code['code'] |
| 1949 |
| 1950 - body = urllib.urlencode({ |
| 1951 - 'grant_type': 'authorization_code', |
| 1952 + post_data = { |
| 1953 'client_id': self.client_id, |
| 1954 'client_secret': self.client_secret, |
| 1955 'code': code, |
| 1956 - 'redirect_uri': self.redirect_uri, |
| 1957 'scope': self.scope, |
| 1958 - }) |
| 1959 + } |
| 1960 + if device_flow_info is not None: |
| 1961 + post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0' |
| 1962 + else: |
| 1963 + post_data['grant_type'] = 'authorization_code' |
| 1964 + post_data['redirect_uri'] = self.redirect_uri |
| 1965 + body = urllib.parse.urlencode(post_data) |
| 1966 headers = { |
| 1967 'content-type': 'application/x-www-form-urlencoded', |
| 1968 } |
| 1969 @@ -1284,26 +1956,31 @@ class OAuth2WebServerFlow(Flow): |
| 1970 if resp.status == 200 and 'access_token' in d: |
| 1971 access_token = d['access_token'] |
| 1972 refresh_token = d.get('refresh_token', None) |
| 1973 + if not refresh_token: |
| 1974 + logger.info( |
| 1975 + 'Received token response with no refresh_token. Consider ' |
| 1976 + "reauthenticating with approval_prompt='force'.") |
| 1977 token_expiry = None |
| 1978 if 'expires_in' in d: |
| 1979 token_expiry = datetime.datetime.utcnow() + datetime.timedelta( |
| 1980 seconds=int(d['expires_in'])) |
| 1981 |
| 1982 + extracted_id_token = None |
| 1983 if 'id_token' in d: |
| 1984 - d['id_token'] = _extract_id_token(d['id_token']) |
| 1985 + extracted_id_token = _extract_id_token(d['id_token']) |
| 1986 |
| 1987 logger.info('Successfully retrieved access token') |
| 1988 return OAuth2Credentials(access_token, self.client_id, |
| 1989 self.client_secret, refresh_token, token_expiry, |
| 1990 self.token_uri, self.user_agent, |
| 1991 revoke_uri=self.revoke_uri, |
| 1992 - id_token=d.get('id_token', None), |
| 1993 + id_token=extracted_id_token, |
| 1994 token_response=d) |
| 1995 else: |
| 1996 - logger.info('Failed to retrieve access token: %s' % content) |
| 1997 + logger.info('Failed to retrieve access token: %s', content) |
| 1998 if 'error' in d: |
| 1999 # you never know what those providers got to say |
| 2000 - error_msg = unicode(d['error']) |
| 2001 + error_msg = str(d['error']) + str(d.get('error_description', '')) |
| 2002 else: |
| 2003 error_msg = 'Invalid response: %s.' % str(resp.status) |
| 2004 raise FlowExchangeError(error_msg) |
| 2005 @@ -1311,7 +1988,8 @@ class OAuth2WebServerFlow(Flow): |
| 2006 |
| 2007 @util.positional(2) |
| 2008 def flow_from_clientsecrets(filename, scope, redirect_uri=None, |
| 2009 - message=None, cache=None): |
| 2010 + message=None, cache=None, login_hint=None, |
| 2011 + device_uri=None): |
| 2012 """Create a Flow from a clientsecrets file. |
| 2013 |
| 2014 Will create the right kind of Flow based on the contents of the clientsecrets |
| 2015 @@ -1329,6 +2007,11 @@ def flow_from_clientsecrets(filename, scope, redirect_uri
=None, |
| 2016 provided then clientsecrets.InvalidClientSecretsError will be raised. |
| 2017 cache: An optional cache service client that implements get() and set() |
| 2018 methods. See clientsecrets.loadfile() for details. |
| 2019 + login_hint: string, Either an email address or domain. Passing this hint |
| 2020 + will either pre-fill the email box on the sign-in form or select the |
| 2021 + proper multi-login session, thereby simplifying the login flow. |
| 2022 + device_uri: string, URI for device authorization endpoint. For convenience |
| 2023 + defaults to Google's endpoints but any OAuth 2.0 provider can be used. |
| 2024 |
| 2025 Returns: |
| 2026 A Flow object. |
| 2027 @@ -1345,10 +2028,13 @@ def flow_from_clientsecrets(filename, scope, redirect_ur
i=None, |
| 2028 'redirect_uri': redirect_uri, |
| 2029 'auth_uri': client_info['auth_uri'], |
| 2030 'token_uri': client_info['token_uri'], |
| 2031 + 'login_hint': login_hint, |
| 2032 } |
| 2033 revoke_uri = client_info.get('revoke_uri') |
| 2034 if revoke_uri is not None: |
| 2035 constructor_kwargs['revoke_uri'] = revoke_uri |
| 2036 + if device_uri is not None: |
| 2037 + constructor_kwargs['device_uri'] = device_uri |
| 2038 return OAuth2WebServerFlow( |
| 2039 client_info['client_id'], client_info['client_secret'], |
| 2040 scope, **constructor_kwargs) |
| 2041 @@ -1360,4 +2046,4 @@ def flow_from_clientsecrets(filename, scope, redirect_uri=
None, |
| 2042 raise |
| 2043 else: |
| 2044 raise UnknownClientSecretsFlowError( |
| 2045 - 'This OAuth 2.0 flow is unsupported: %r' % client_type) |
| 2046 + 'This OAuth 2.0 flow is unsupported: %r' % client_type) |
| 2047 \ No newline at end of file |
| 2048 diff --git a/third_party/oauth2client/clientsecrets.py b/third_party/oauth2clien
t/clientsecrets.py |
| 2049 index ac99aae..af079ec 100644 |
| 2050 --- a/third_party/oauth2client/clientsecrets.py |
| 2051 +++ b/third_party/oauth2client/clientsecrets.py |
| 2052 @@ -1,4 +1,4 @@ |
| 2053 -# Copyright (C) 2011 Google Inc. |
| 2054 +# Copyright 2014 Google Inc. All rights reserved. |
| 2055 # |
| 2056 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 2057 # you may not use this file except in compliance with the License. |
| 2058 @@ -20,8 +20,10 @@ an OAuth 2.0 protected service. |
| 2059 |
| 2060 __author__ = 'jcgregorio@google.com (Joe Gregorio)' |
| 2061 |
| 2062 +import json |
| 2063 + |
| 2064 +from third_party import six |
| 2065 |
| 2066 -from anyjson import simplejson |
| 2067 |
| 2068 # Properties that make a client_secrets.json file valid. |
| 2069 TYPE_WEB = 'web' |
| 2070 @@ -68,11 +70,21 @@ class InvalidClientSecretsError(Error): |
| 2071 |
| 2072 |
| 2073 def _validate_clientsecrets(obj): |
| 2074 - if obj is None or len(obj) != 1: |
| 2075 - raise InvalidClientSecretsError('Invalid file format.') |
| 2076 - client_type = obj.keys()[0] |
| 2077 - if client_type not in VALID_CLIENT.keys(): |
| 2078 - raise InvalidClientSecretsError('Unknown client type: %s.' % client_type) |
| 2079 + _INVALID_FILE_FORMAT_MSG = ( |
| 2080 + 'Invalid file format. See ' |
| 2081 + 'https://developers.google.com/api-client-library/' |
| 2082 + 'python/guide/aaa_client_secrets') |
| 2083 + |
| 2084 + if obj is None: |
| 2085 + raise InvalidClientSecretsError(_INVALID_FILE_FORMAT_MSG) |
| 2086 + if len(obj) != 1: |
| 2087 + raise InvalidClientSecretsError( |
| 2088 + _INVALID_FILE_FORMAT_MSG + ' ' |
| 2089 + 'Expected a JSON object with a single property for a "web" or ' |
| 2090 + '"installed" application') |
| 2091 + client_type = tuple(obj)[0] |
| 2092 + if client_type not in VALID_CLIENT: |
| 2093 + raise InvalidClientSecretsError('Unknown client type: %s.' % (client_type,)
) |
| 2094 client_info = obj[client_type] |
| 2095 for prop_name in VALID_CLIENT[client_type]['required']: |
| 2096 if prop_name not in client_info: |
| 2097 @@ -87,22 +99,19 @@ def _validate_clientsecrets(obj): |
| 2098 |
| 2099 |
| 2100 def load(fp): |
| 2101 - obj = simplejson.load(fp) |
| 2102 + obj = json.load(fp) |
| 2103 return _validate_clientsecrets(obj) |
| 2104 |
| 2105 |
| 2106 def loads(s): |
| 2107 - obj = simplejson.loads(s) |
| 2108 + obj = json.loads(s) |
| 2109 return _validate_clientsecrets(obj) |
| 2110 |
| 2111 |
| 2112 def _loadfile(filename): |
| 2113 try: |
| 2114 - fp = file(filename, 'r') |
| 2115 - try: |
| 2116 - obj = simplejson.load(fp) |
| 2117 - finally: |
| 2118 - fp.close() |
| 2119 + with open(filename, 'r') as fp: |
| 2120 + obj = json.load(fp) |
| 2121 except IOError: |
| 2122 raise InvalidClientSecretsError('File not found: "%s"' % filename) |
| 2123 return _validate_clientsecrets(obj) |
| 2124 @@ -114,10 +123,12 @@ def loadfile(filename, cache=None): |
| 2125 Typical cache storage would be App Engine memcache service, |
| 2126 but you can pass in any other cache client that implements |
| 2127 these methods: |
| 2128 - - get(key, namespace=ns) |
| 2129 - - set(key, value, namespace=ns) |
| 2130 |
| 2131 - Usage: |
| 2132 + * ``get(key, namespace=ns)`` |
| 2133 + * ``set(key, value, namespace=ns)`` |
| 2134 + |
| 2135 + Usage:: |
| 2136 + |
| 2137 # without caching |
| 2138 client_type, client_info = loadfile('secrets.json') |
| 2139 # using App Engine memcache service |
| 2140 @@ -150,4 +161,4 @@ def loadfile(filename, cache=None): |
| 2141 obj = {client_type: client_info} |
| 2142 cache.set(filename, obj, namespace=_SECRET_NAMESPACE) |
| 2143 |
| 2144 - return obj.iteritems().next() |
| 2145 + return next(six.iteritems(obj)) |
| 2146 \ No newline at end of file |
| 2147 diff --git a/third_party/oauth2client/crypt.py b/third_party/oauth2client/crypt.
py |
| 2148 index 2d31815..b5b8cab 100644 |
| 2149 --- a/third_party/oauth2client/crypt.py |
| 2150 +++ b/third_party/oauth2client/crypt.py |
| 2151 @@ -1,7 +1,6 @@ |
| 2152 -#!/usr/bin/python2.4 |
| 2153 # -*- coding: utf-8 -*- |
| 2154 # |
| 2155 -# Copyright (C) 2011 Google Inc. |
| 2156 +# Copyright 2014 Google Inc. All rights reserved. |
| 2157 # |
| 2158 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 2159 # you may not use this file except in compliance with the License. |
| 2160 @@ -14,13 +13,15 @@ |
| 2161 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 2162 # See the License for the specific language governing permissions and |
| 2163 # limitations under the License. |
| 2164 +"""Crypto-related routines for oauth2client.""" |
| 2165 |
| 2166 import base64 |
| 2167 -import hashlib |
| 2168 +import json |
| 2169 import logging |
| 2170 +import sys |
| 2171 import time |
| 2172 |
| 2173 -from anyjson import simplejson |
| 2174 +from third_party import six |
| 2175 |
| 2176 |
| 2177 CLOCK_SKEW_SECS = 300 # 5 minutes in seconds |
| 2178 @@ -38,7 +39,6 @@ class AppIdentityError(Exception): |
| 2179 try: |
| 2180 from OpenSSL import crypto |
| 2181 |
| 2182 - |
| 2183 class OpenSSLVerifier(object): |
| 2184 """Verifies the signature on a message.""" |
| 2185 |
| 2186 @@ -62,6 +62,8 @@ try: |
| 2187 key that this object was constructed with. |
| 2188 """ |
| 2189 try: |
| 2190 + if isinstance(message, six.text_type): |
| 2191 + message = message.encode('utf-8') |
| 2192 crypto.verify(self._pubkey, signature, message, 'sha256') |
| 2193 return True |
| 2194 except: |
| 2195 @@ -104,15 +106,17 @@ try: |
| 2196 """Signs a message. |
| 2197 |
| 2198 Args: |
| 2199 - message: string, Message to be signed. |
| 2200 + message: bytes, Message to be signed. |
| 2201 |
| 2202 Returns: |
| 2203 string, The signature of the message for the given key. |
| 2204 """ |
| 2205 + if isinstance(message, six.text_type): |
| 2206 + message = message.encode('utf-8') |
| 2207 return crypto.sign(self._key, message, 'sha256') |
| 2208 |
| 2209 @staticmethod |
| 2210 - def from_string(key, password='notasecret'): |
| 2211 + def from_string(key, password=b'notasecret'): |
| 2212 """Construct a Signer instance from a string. |
| 2213 |
| 2214 Args: |
| 2215 @@ -125,21 +129,45 @@ try: |
| 2216 Raises: |
| 2217 OpenSSL.crypto.Error if the key can't be parsed. |
| 2218 """ |
| 2219 - if key.startswith('-----BEGIN '): |
| 2220 - pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key) |
| 2221 + parsed_pem_key = _parse_pem_key(key) |
| 2222 + if parsed_pem_key: |
| 2223 + pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key) |
| 2224 else: |
| 2225 + if isinstance(password, six.text_type): |
| 2226 + password = password.encode('utf-8') |
| 2227 pkey = crypto.load_pkcs12(key, password).get_privatekey() |
| 2228 return OpenSSLSigner(pkey) |
| 2229 |
| 2230 + |
| 2231 + def pkcs12_key_as_pem(private_key_text, private_key_password): |
| 2232 + """Convert the contents of a PKCS12 key to PEM using OpenSSL. |
| 2233 + |
| 2234 + Args: |
| 2235 + private_key_text: String. Private key. |
| 2236 + private_key_password: String. Password for PKCS12. |
| 2237 + |
| 2238 + Returns: |
| 2239 + String. PEM contents of ``private_key_text``. |
| 2240 + """ |
| 2241 + decoded_body = base64.b64decode(private_key_text) |
| 2242 + if isinstance(private_key_password, six.string_types): |
| 2243 + private_key_password = private_key_password.encode('ascii') |
| 2244 + |
| 2245 + pkcs12 = crypto.load_pkcs12(decoded_body, private_key_password) |
| 2246 + return crypto.dump_privatekey(crypto.FILETYPE_PEM, |
| 2247 + pkcs12.get_privatekey()) |
| 2248 except ImportError: |
| 2249 OpenSSLVerifier = None |
| 2250 OpenSSLSigner = None |
| 2251 + def pkcs12_key_as_pem(*args, **kwargs): |
| 2252 + raise NotImplementedError('pkcs12_key_as_pem requires OpenSSL.') |
| 2253 |
| 2254 |
| 2255 try: |
| 2256 from Crypto.PublicKey import RSA |
| 2257 from Crypto.Hash import SHA256 |
| 2258 from Crypto.Signature import PKCS1_v1_5 |
| 2259 + from Crypto.Util.asn1 import DerSequence |
| 2260 |
| 2261 |
| 2262 class PyCryptoVerifier(object): |
| 2263 @@ -181,14 +209,17 @@ try: |
| 2264 |
| 2265 Returns: |
| 2266 Verifier instance. |
| 2267 - |
| 2268 - Raises: |
| 2269 - NotImplementedError if is_x509_cert is true. |
| 2270 """ |
| 2271 if is_x509_cert: |
| 2272 - raise NotImplementedError( |
| 2273 - 'X509 certs are not supported by the PyCrypto library. ' |
| 2274 - 'Try using PyOpenSSL if native code is an option.') |
| 2275 + if isinstance(key_pem, six.text_type): |
| 2276 + key_pem = key_pem.encode('ascii') |
| 2277 + pemLines = key_pem.replace(b' ', b'').split() |
| 2278 + certDer = _urlsafe_b64decode(b''.join(pemLines[1:-1])) |
| 2279 + certSeq = DerSequence() |
| 2280 + certSeq.decode(certDer) |
| 2281 + tbsSeq = DerSequence() |
| 2282 + tbsSeq.decode(certSeq[0]) |
| 2283 + pubkey = RSA.importKey(tbsSeq[6]) |
| 2284 else: |
| 2285 pubkey = RSA.importKey(key_pem) |
| 2286 return PyCryptoVerifier(pubkey) |
| 2287 @@ -214,6 +245,8 @@ try: |
| 2288 Returns: |
| 2289 string, The signature of the message for the given key. |
| 2290 """ |
| 2291 + if isinstance(message, six.text_type): |
| 2292 + message = message.encode('utf-8') |
| 2293 return PKCS1_v1_5.new(self._key).sign(SHA256.new(message)) |
| 2294 |
| 2295 @staticmethod |
| 2296 @@ -230,11 +263,12 @@ try: |
| 2297 Raises: |
| 2298 NotImplementedError if they key isn't in PEM format. |
| 2299 """ |
| 2300 - if key.startswith('-----BEGIN '): |
| 2301 - pkey = RSA.importKey(key) |
| 2302 + parsed_pem_key = _parse_pem_key(key) |
| 2303 + if parsed_pem_key: |
| 2304 + pkey = RSA.importKey(parsed_pem_key) |
| 2305 else: |
| 2306 raise NotImplementedError( |
| 2307 - 'PKCS12 format is not supported by the PyCrpto library. ' |
| 2308 + 'PKCS12 format is not supported by the PyCrypto library. ' |
| 2309 'Try converting to a "PEM" ' |
| 2310 '(openssl pkcs12 -in xxxxx.p12 -nodes -nocerts > privatekey.pem) ' |
| 2311 'or using PyOpenSSL if native code is an option.') |
| 2312 @@ -256,19 +290,39 @@ else: |
| 2313 'PyOpenSSL, or PyCrypto 2.6 or later') |
| 2314 |
| 2315 |
| 2316 +def _parse_pem_key(raw_key_input): |
| 2317 + """Identify and extract PEM keys. |
| 2318 + |
| 2319 + Determines whether the given key is in the format of PEM key, and extracts |
| 2320 + the relevant part of the key if it is. |
| 2321 + |
| 2322 + Args: |
| 2323 + raw_key_input: The contents of a private key file (either PEM or PKCS12). |
| 2324 + |
| 2325 + Returns: |
| 2326 + string, The actual key if the contents are from a PEM file, or else None. |
| 2327 + """ |
| 2328 + offset = raw_key_input.find(b'-----BEGIN ') |
| 2329 + if offset != -1: |
| 2330 + return raw_key_input[offset:] |
| 2331 + |
| 2332 + |
| 2333 def _urlsafe_b64encode(raw_bytes): |
| 2334 - return base64.urlsafe_b64encode(raw_bytes).rstrip('=') |
| 2335 + if isinstance(raw_bytes, six.text_type): |
| 2336 + raw_bytes = raw_bytes.encode('utf-8') |
| 2337 + return base64.urlsafe_b64encode(raw_bytes).decode('ascii').rstrip('=') |
| 2338 |
| 2339 |
| 2340 def _urlsafe_b64decode(b64string): |
| 2341 # Guard against unicode strings, which base64 can't handle. |
| 2342 - b64string = b64string.encode('ascii') |
| 2343 - padded = b64string + '=' * (4 - len(b64string) % 4) |
| 2344 + if isinstance(b64string, six.text_type): |
| 2345 + b64string = b64string.encode('ascii') |
| 2346 + padded = b64string + b'=' * (4 - len(b64string) % 4) |
| 2347 return base64.urlsafe_b64decode(padded) |
| 2348 |
| 2349 |
| 2350 def _json_encode(data): |
| 2351 - return simplejson.dumps(data, separators = (',', ':')) |
| 2352 + return json.dumps(data, separators=(',', ':')) |
| 2353 |
| 2354 |
| 2355 def make_signed_jwt(signer, payload): |
| 2356 @@ -286,8 +340,8 @@ def make_signed_jwt(signer, payload): |
| 2357 header = {'typ': 'JWT', 'alg': 'RS256'} |
| 2358 |
| 2359 segments = [ |
| 2360 - _urlsafe_b64encode(_json_encode(header)), |
| 2361 - _urlsafe_b64encode(_json_encode(payload)), |
| 2362 + _urlsafe_b64encode(_json_encode(header)), |
| 2363 + _urlsafe_b64encode(_json_encode(payload)), |
| 2364 ] |
| 2365 signing_input = '.'.join(segments) |
| 2366 |
| 2367 @@ -318,9 +372,8 @@ def verify_signed_jwt_with_certs(jwt, certs, audience): |
| 2368 """ |
| 2369 segments = jwt.split('.') |
| 2370 |
| 2371 - if (len(segments) != 3): |
| 2372 - raise AppIdentityError( |
| 2373 - 'Wrong number of segments in token: %s' % jwt) |
| 2374 + if len(segments) != 3: |
| 2375 + raise AppIdentityError('Wrong number of segments in token: %s' % jwt) |
| 2376 signed = '%s.%s' % (segments[0], segments[1]) |
| 2377 |
| 2378 signature = _urlsafe_b64decode(segments[2]) |
| 2379 @@ -328,15 +381,15 @@ def verify_signed_jwt_with_certs(jwt, certs, audience): |
| 2380 # Parse token. |
| 2381 json_body = _urlsafe_b64decode(segments[1]) |
| 2382 try: |
| 2383 - parsed = simplejson.loads(json_body) |
| 2384 + parsed = json.loads(json_body.decode('utf-8')) |
| 2385 except: |
| 2386 raise AppIdentityError('Can\'t parse token: %s' % json_body) |
| 2387 |
| 2388 # Check signature. |
| 2389 verified = False |
| 2390 - for (keyname, pem) in certs.items(): |
| 2391 + for pem in certs.values(): |
| 2392 verifier = Verifier.from_string(pem, True) |
| 2393 - if (verifier.verify(signed, signature)): |
| 2394 + if verifier.verify(signed, signature): |
| 2395 verified = True |
| 2396 break |
| 2397 if not verified: |
| 2398 @@ -349,21 +402,20 @@ def verify_signed_jwt_with_certs(jwt, certs, audience): |
| 2399 earliest = iat - CLOCK_SKEW_SECS |
| 2400 |
| 2401 # Check expiration timestamp. |
| 2402 - now = long(time.time()) |
| 2403 + now = int(time.time()) |
| 2404 exp = parsed.get('exp') |
| 2405 if exp is None: |
| 2406 raise AppIdentityError('No exp field in token: %s' % json_body) |
| 2407 if exp >= now + MAX_TOKEN_LIFETIME_SECS: |
| 2408 - raise AppIdentityError( |
| 2409 - 'exp field too far in future: %s' % json_body) |
| 2410 + raise AppIdentityError('exp field too far in future: %s' % json_body) |
| 2411 latest = exp + CLOCK_SKEW_SECS |
| 2412 |
| 2413 if now < earliest: |
| 2414 raise AppIdentityError('Token used too early, %d < %d: %s' % |
| 2415 - (now, earliest, json_body)) |
| 2416 + (now, earliest, json_body)) |
| 2417 if now > latest: |
| 2418 raise AppIdentityError('Token used too late, %d > %d: %s' % |
| 2419 - (now, latest, json_body)) |
| 2420 + (now, latest, json_body)) |
| 2421 |
| 2422 # Check audience. |
| 2423 if audience is not None: |
| 2424 @@ -372,6 +424,6 @@ def verify_signed_jwt_with_certs(jwt, certs, audience): |
| 2425 raise AppIdentityError('No aud field in token: %s' % json_body) |
| 2426 if aud != audience: |
| 2427 raise AppIdentityError('Wrong recipient, %s != %s: %s' % |
| 2428 - (aud, audience, json_body)) |
| 2429 + (aud, audience, json_body)) |
| 2430 |
| 2431 - return parsed |
| 2432 + return parsed |
| 2433 \ No newline at end of file |
| 2434 diff --git a/third_party/oauth2client/devshell.py b/third_party/oauth2client/dev
shell.py |
| 2435 new file mode 100644 |
| 2436 index 0000000..69d3808 |
| 2437 --- /dev/null |
| 2438 +++ b/third_party/oauth2client/devshell.py |
| 2439 @@ -0,0 +1,135 @@ |
| 2440 +# Copyright 2015 Google Inc. All Rights Reserved. |
| 2441 +# |
| 2442 +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 2443 +# you may not use this file except in compliance with the License. |
| 2444 +# You may obtain a copy of the License at |
| 2445 +# |
| 2446 +# http://www.apache.org/licenses/LICENSE-2.0 |
| 2447 +# |
| 2448 +# Unless required by applicable law or agreed to in writing, software |
| 2449 +# distributed under the License is distributed on an "AS IS" BASIS, |
| 2450 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 2451 +# See the License for the specific language governing permissions and |
| 2452 +# limitations under the License. |
| 2453 + |
| 2454 +"""OAuth 2.0 utitilies for Google Developer Shell environment.""" |
| 2455 + |
| 2456 +import json |
| 2457 +import os |
| 2458 + |
| 2459 +from . import client |
| 2460 + |
| 2461 + |
| 2462 +DEVSHELL_ENV = 'DEVSHELL_CLIENT_PORT' |
| 2463 + |
| 2464 + |
| 2465 +class Error(Exception): |
| 2466 + """Errors for this module.""" |
| 2467 + pass |
| 2468 + |
| 2469 + |
| 2470 +class CommunicationError(Error): |
| 2471 + """Errors for communication with the Developer Shell server.""" |
| 2472 + |
| 2473 + |
| 2474 +class NoDevshellServer(Error): |
| 2475 + """Error when no Developer Shell server can be contacted.""" |
| 2476 + |
| 2477 + |
| 2478 +# The request for credential information to the Developer Shell client socket i
s |
| 2479 +# always an empty PBLite-formatted JSON object, so just define it as a constant
. |
| 2480 +CREDENTIAL_INFO_REQUEST_JSON = '[]' |
| 2481 + |
| 2482 + |
| 2483 +class CredentialInfoResponse(object): |
| 2484 + """Credential information response from Developer Shell server. |
| 2485 + |
| 2486 + The credential information response from Developer Shell socket is a |
| 2487 + PBLite-formatted JSON array with fields encoded by their index in the array: |
| 2488 + * Index 0 - user email |
| 2489 + * Index 1 - default project ID. None if the project context is not known. |
| 2490 + * Index 2 - OAuth2 access token. None if there is no valid auth context. |
| 2491 + """ |
| 2492 + |
| 2493 + def __init__(self, json_string): |
| 2494 + """Initialize the response data from JSON PBLite array.""" |
| 2495 + pbl = json.loads(json_string) |
| 2496 + if not isinstance(pbl, list): |
| 2497 + raise ValueError('Not a list: ' + str(pbl)) |
| 2498 + pbl_len = len(pbl) |
| 2499 + self.user_email = pbl[0] if pbl_len > 0 else None |
| 2500 + self.project_id = pbl[1] if pbl_len > 1 else None |
| 2501 + self.access_token = pbl[2] if pbl_len > 2 else None |
| 2502 + |
| 2503 + |
| 2504 +def _SendRecv(): |
| 2505 + """Communicate with the Developer Shell server socket.""" |
| 2506 + |
| 2507 + port = int(os.getenv(DEVSHELL_ENV, 0)) |
| 2508 + if port == 0: |
| 2509 + raise NoDevshellServer() |
| 2510 + |
| 2511 + import socket |
| 2512 + |
| 2513 + sock = socket.socket() |
| 2514 + sock.connect(('localhost', port)) |
| 2515 + |
| 2516 + data = CREDENTIAL_INFO_REQUEST_JSON |
| 2517 + msg = '%s\n%s' % (len(data), data) |
| 2518 + sock.sendall(msg.encode()) |
| 2519 + |
| 2520 + header = sock.recv(6).decode() |
| 2521 + if '\n' not in header: |
| 2522 + raise CommunicationError('saw no newline in the first 6 bytes') |
| 2523 + len_str, json_str = header.split('\n', 1) |
| 2524 + to_read = int(len_str) - len(json_str) |
| 2525 + if to_read > 0: |
| 2526 + json_str += sock.recv(to_read, socket.MSG_WAITALL).decode() |
| 2527 + |
| 2528 + return CredentialInfoResponse(json_str) |
| 2529 + |
| 2530 + |
| 2531 +class DevshellCredentials(client.GoogleCredentials): |
| 2532 + """Credentials object for Google Developer Shell environment. |
| 2533 + |
| 2534 + This object will allow a Google Developer Shell session to identify its user |
| 2535 + to Google and other OAuth 2.0 servers that can verify assertions. It can be |
| 2536 + used for the purpose of accessing data stored under the user account. |
| 2537 + |
| 2538 + This credential does not require a flow to instantiate because it represents |
| 2539 + a two legged flow, and therefore has all of the required information to |
| 2540 + generate and refresh its own access tokens. |
| 2541 + """ |
| 2542 + |
| 2543 + def __init__(self, user_agent=None): |
| 2544 + super(DevshellCredentials, self).__init__( |
| 2545 + None, # access_token, initialized below |
| 2546 + None, # client_id |
| 2547 + None, # client_secret |
| 2548 + None, # refresh_token |
| 2549 + None, # token_expiry |
| 2550 + None, # token_uri |
| 2551 + user_agent) |
| 2552 + self._refresh(None) |
| 2553 + |
| 2554 + def _refresh(self, http_request): |
| 2555 + self.devshell_response = _SendRecv() |
| 2556 + self.access_token = self.devshell_response.access_token |
| 2557 + |
| 2558 + @property |
| 2559 + def user_email(self): |
| 2560 + return self.devshell_response.user_email |
| 2561 + |
| 2562 + @property |
| 2563 + def project_id(self): |
| 2564 + return self.devshell_response.project_id |
| 2565 + |
| 2566 + @classmethod |
| 2567 + def from_json(cls, json_data): |
| 2568 + raise NotImplementedError( |
| 2569 + 'Cannot load Developer Shell credentials from JSON.') |
| 2570 + |
| 2571 + @property |
| 2572 + def serialization_data(self): |
| 2573 + raise NotImplementedError( |
| 2574 + 'Cannot serialize Developer Shell credentials.') |
| 2575 diff --git a/third_party/oauth2client/django_orm.py b/third_party/oauth2client/d
jango_orm.py |
| 2576 index d54d20c..ffbcf92 100644 |
| 2577 --- a/third_party/oauth2client/django_orm.py |
| 2578 +++ b/third_party/oauth2client/django_orm.py |
| 2579 @@ -1,4 +1,4 @@ |
| 2580 -# Copyright (C) 2010 Google Inc. |
| 2581 +# Copyright 2014 Google Inc. All rights reserved. |
| 2582 # |
| 2583 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 2584 # you may not use this file except in compliance with the License. |
| 2585 @@ -116,14 +116,21 @@ class Storage(BaseStorage): |
| 2586 credential.set_store(self) |
| 2587 return credential |
| 2588 |
| 2589 - def locked_put(self, credentials): |
| 2590 + def locked_put(self, credentials, overwrite=False): |
| 2591 """Write a Credentials to the datastore. |
| 2592 |
| 2593 Args: |
| 2594 credentials: Credentials, the credentials to store. |
| 2595 + overwrite: Boolean, indicates whether you would like these credentials to |
| 2596 + overwrite any existing stored credentials. |
| 2597 """ |
| 2598 args = {self.key_name: self.key_value} |
| 2599 - entity = self.model_class(**args) |
| 2600 + |
| 2601 + if overwrite: |
| 2602 + entity, unused_is_new = self.model_class.objects.get_or_create(**args) |
| 2603 + else: |
| 2604 + entity = self.model_class(**args) |
| 2605 + |
| 2606 setattr(entity, self.property_name, credentials) |
| 2607 entity.save() |
| 2608 |
| 2609 @@ -131,4 +138,4 @@ class Storage(BaseStorage): |
| 2610 """Delete Credentials from the datastore.""" |
| 2611 |
| 2612 query = {self.key_name: self.key_value} |
| 2613 - entities = self.model_class.objects.filter(**query).delete() |
| 2614 + entities = self.model_class.objects.filter(**query).delete() |
| 2615 \ No newline at end of file |
| 2616 diff --git a/third_party/oauth2client/file.py b/third_party/oauth2client/file.py |
| 2617 index 1895f94..4337fcf 100644 |
| 2618 --- a/third_party/oauth2client/file.py |
| 2619 +++ b/third_party/oauth2client/file.py |
| 2620 @@ -1,4 +1,4 @@ |
| 2621 -# Copyright (C) 2010 Google Inc. |
| 2622 +# Copyright 2014 Google Inc. All rights reserved. |
| 2623 # |
| 2624 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 2625 # you may not use this file except in compliance with the License. |
| 2626 @@ -21,12 +21,10 @@ credentials. |
| 2627 __author__ = 'jcgregorio@google.com (Joe Gregorio)' |
| 2628 |
| 2629 import os |
| 2630 -import stat |
| 2631 import threading |
| 2632 |
| 2633 -from anyjson import simplejson |
| 2634 -from client import Storage as BaseStorage |
| 2635 from client import Credentials |
| 2636 +from client import Storage as BaseStorage |
| 2637 |
| 2638 |
| 2639 class CredentialsFileSymbolicLinkError(Exception): |
| 2640 @@ -92,7 +90,7 @@ class Storage(BaseStorage): |
| 2641 simple version of "touch" to ensure the file has been created. |
| 2642 """ |
| 2643 if not os.path.exists(self._filename): |
| 2644 - old_umask = os.umask(0177) |
| 2645 + old_umask = os.umask(0o177) |
| 2646 try: |
| 2647 open(self._filename, 'a+b').close() |
| 2648 finally: |
| 2649 @@ -110,7 +108,7 @@ class Storage(BaseStorage): |
| 2650 |
| 2651 self._create_file_if_needed() |
| 2652 self._validate_file() |
| 2653 - f = open(self._filename, 'wb') |
| 2654 + f = open(self._filename, 'w') |
| 2655 f.write(credentials.to_json()) |
| 2656 f.close() |
| 2657 |
| 2658 @@ -121,4 +119,4 @@ class Storage(BaseStorage): |
| 2659 credentials: Credentials, the credentials to store. |
| 2660 """ |
| 2661 |
| 2662 - os.unlink(self._filename) |
| 2663 + os.unlink(self._filename) |
| 2664 \ No newline at end of file |
| 2665 diff --git a/third_party/oauth2client/gce.py b/third_party/oauth2client/gce.py |
| 2666 index c7fd7c1..ebd14e3 100644 |
| 2667 --- a/third_party/oauth2client/gce.py |
| 2668 +++ b/third_party/oauth2client/gce.py |
| 2669 @@ -1,4 +1,4 @@ |
| 2670 -# Copyright (C) 2012 Google Inc. |
| 2671 +# Copyright 2014 Google Inc. All rights reserved. |
| 2672 # |
| 2673 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 2674 # you may not use this file except in compliance with the License. |
| 2675 @@ -19,14 +19,13 @@ Utilities for making it easier to use OAuth 2.0 on Google Co
mpute Engine. |
| 2676 |
| 2677 __author__ = 'jcgregorio@google.com (Joe Gregorio)' |
| 2678 |
| 2679 -import httplib2 |
| 2680 +import json |
| 2681 import logging |
| 2682 -import uritemplate |
| 2683 +from third_party.six.moves import urllib |
| 2684 |
| 2685 -from oauth2client import util |
| 2686 -from oauth2client.anyjson import simplejson |
| 2687 -from oauth2client.client import AccessTokenRefreshError |
| 2688 -from oauth2client.client import AssertionCredentials |
| 2689 +from third_party.oauth2client import util |
| 2690 +from third_party.oauth2client.client import AccessTokenRefreshError |
| 2691 +from third_party.oauth2client.client import AssertionCredentials |
| 2692 |
| 2693 logger = logging.getLogger(__name__) |
| 2694 |
| 2695 @@ -57,13 +56,14 @@ class AppAssertionCredentials(AssertionCredentials): |
| 2696 requested. |
| 2697 """ |
| 2698 self.scope = util.scopes_to_string(scope) |
| 2699 + self.kwargs = kwargs |
| 2700 |
| 2701 # Assertion type is no longer used, but still in the parent class signature
. |
| 2702 super(AppAssertionCredentials, self).__init__(None) |
| 2703 |
| 2704 @classmethod |
| 2705 - def from_json(cls, json): |
| 2706 - data = simplejson.loads(json) |
| 2707 + def from_json(cls, json_data): |
| 2708 + data = json.loads(json_data) |
| 2709 return AppAssertionCredentials(data['scope']) |
| 2710 |
| 2711 def _refresh(self, http_request): |
| 2712 @@ -78,13 +78,28 @@ class AppAssertionCredentials(AssertionCredentials): |
| 2713 Raises: |
| 2714 AccessTokenRefreshError: When the refresh fails. |
| 2715 """ |
| 2716 - uri = uritemplate.expand(META, {'scope': self.scope}) |
| 2717 + query = '?scope=%s' % urllib.parse.quote(self.scope, '') |
| 2718 + uri = META.replace('{?scope}', query) |
| 2719 response, content = http_request(uri) |
| 2720 if response.status == 200: |
| 2721 try: |
| 2722 - d = simplejson.loads(content) |
| 2723 - except StandardError, e: |
| 2724 + d = json.loads(content) |
| 2725 + except Exception as e: |
| 2726 raise AccessTokenRefreshError(str(e)) |
| 2727 self.access_token = d['accessToken'] |
| 2728 else: |
| 2729 + if response.status == 404: |
| 2730 + content += (' This can occur if a VM was created' |
| 2731 + ' with no service account or scopes.') |
| 2732 raise AccessTokenRefreshError(content) |
| 2733 + |
| 2734 + @property |
| 2735 + def serialization_data(self): |
| 2736 + raise NotImplementedError( |
| 2737 + 'Cannot serialize credentials for GCE service accounts.') |
| 2738 + |
| 2739 + def create_scoped_required(self): |
| 2740 + return not self.scope |
| 2741 + |
| 2742 + def create_scoped(self, scopes): |
| 2743 + return AppAssertionCredentials(scopes, **self.kwargs) |
| 2744 \ No newline at end of file |
| 2745 diff --git a/third_party/oauth2client/keyring_storage.py b/third_party/oauth2cli
ent/keyring_storage.py |
| 2746 index efe2949..512ac77 100644 |
| 2747 --- a/third_party/oauth2client/keyring_storage.py |
| 2748 +++ b/third_party/oauth2client/keyring_storage.py |
| 2749 @@ -1,4 +1,4 @@ |
| 2750 -# Copyright (C) 2012 Google Inc. |
| 2751 +# Copyright 2014 Google Inc. All rights reserved. |
| 2752 # |
| 2753 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 2754 # you may not use this file except in compliance with the License. |
| 2755 @@ -19,11 +19,12 @@ A Storage for Credentials that uses the keyring module. |
| 2756 |
| 2757 __author__ = 'jcgregorio@google.com (Joe Gregorio)' |
| 2758 |
| 2759 -import keyring |
| 2760 import threading |
| 2761 |
| 2762 -from client import Storage as BaseStorage |
| 2763 +import keyring |
| 2764 + |
| 2765 from client import Credentials |
| 2766 +from client import Storage as BaseStorage |
| 2767 |
| 2768 |
| 2769 class Storage(BaseStorage): |
| 2770 @@ -106,4 +107,4 @@ class Storage(BaseStorage): |
| 2771 Args: |
| 2772 credentials: Credentials, the credentials to store. |
| 2773 """ |
| 2774 - keyring.set_password(self._service_name, self._user_name, '') |
| 2775 + keyring.set_password(self._service_name, self._user_name, '') |
| 2776 \ No newline at end of file |
| 2777 diff --git a/third_party/oauth2client/locked_file.py b/third_party/oauth2client/
locked_file.py |
| 2778 index 858b702..fa2c6c1 100644 |
| 2779 --- a/third_party/oauth2client/locked_file.py |
| 2780 +++ b/third_party/oauth2client/locked_file.py |
| 2781 @@ -1,4 +1,4 @@ |
| 2782 -# Copyright 2011 Google Inc. |
| 2783 +# Copyright 2014 Google Inc. All rights reserved. |
| 2784 # |
| 2785 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 2786 # you may not use this file except in compliance with the License. |
| 2787 @@ -17,17 +17,21 @@ |
| 2788 This module first tries to use fcntl locking to ensure serialized access |
| 2789 to a file, then falls back on a lock file if that is unavialable. |
| 2790 |
| 2791 -Usage: |
| 2792 +Usage:: |
| 2793 + |
| 2794 f = LockedFile('filename', 'r+b', 'rb') |
| 2795 f.open_and_lock() |
| 2796 if f.is_locked(): |
| 2797 - print 'Acquired filename with r+b mode' |
| 2798 + print('Acquired filename with r+b mode') |
| 2799 f.file_handle().write('locked data') |
| 2800 else: |
| 2801 - print 'Aquired filename with rb mode' |
| 2802 + print('Acquired filename with rb mode') |
| 2803 f.unlock_and_close() |
| 2804 + |
| 2805 """ |
| 2806 |
| 2807 +from __future__ import print_function |
| 2808 + |
| 2809 __author__ = 'cache@google.com (David T McWherter)' |
| 2810 |
| 2811 import errno |
| 2812 @@ -70,6 +74,7 @@ class _Opener(object): |
| 2813 self._mode = mode |
| 2814 self._fallback_mode = fallback_mode |
| 2815 self._fh = None |
| 2816 + self._lock_fd = None |
| 2817 |
| 2818 def is_locked(self): |
| 2819 """Was the file locked.""" |
| 2820 @@ -122,7 +127,7 @@ class _PosixOpener(_Opener): |
| 2821 validate_file(self._filename) |
| 2822 try: |
| 2823 self._fh = open(self._filename, self._mode) |
| 2824 - except IOError, e: |
| 2825 + except IOError as e: |
| 2826 # If we can't access with _mode, try _fallback_mode and don't lock. |
| 2827 if e.errno == errno.EACCES: |
| 2828 self._fh = open(self._filename, self._fallback_mode) |
| 2829 @@ -137,12 +142,12 @@ class _PosixOpener(_Opener): |
| 2830 self._locked = True |
| 2831 break |
| 2832 |
| 2833 - except OSError, e: |
| 2834 + except OSError as e: |
| 2835 if e.errno != errno.EEXIST: |
| 2836 raise |
| 2837 if (time.time() - start_time) >= timeout: |
| 2838 - logger.warn('Could not acquire lock %s in %s seconds' % ( |
| 2839 - lock_filename, timeout)) |
| 2840 + logger.warn('Could not acquire lock %s in %s seconds', |
| 2841 + lock_filename, timeout) |
| 2842 # Close the file and open in fallback_mode. |
| 2843 if self._fh: |
| 2844 self._fh.close() |
| 2845 @@ -192,9 +197,9 @@ try: |
| 2846 validate_file(self._filename) |
| 2847 try: |
| 2848 self._fh = open(self._filename, self._mode) |
| 2849 - except IOError, e: |
| 2850 + except IOError as e: |
| 2851 # If we can't access with _mode, try _fallback_mode and don't lock. |
| 2852 - if e.errno == errno.EACCES: |
| 2853 + if e.errno in (errno.EPERM, errno.EACCES): |
| 2854 self._fh = open(self._filename, self._fallback_mode) |
| 2855 return |
| 2856 |
| 2857 @@ -204,16 +209,16 @@ try: |
| 2858 fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX) |
| 2859 self._locked = True |
| 2860 return |
| 2861 - except IOError, e: |
| 2862 + except IOError as e: |
| 2863 # If not retrying, then just pass on the error. |
| 2864 if timeout == 0: |
| 2865 - raise e |
| 2866 + raise |
| 2867 if e.errno != errno.EACCES: |
| 2868 - raise e |
| 2869 + raise |
| 2870 # We could not acquire the lock. Try again. |
| 2871 if (time.time() - start_time) >= timeout: |
| 2872 - logger.warn('Could not lock %s in %s seconds' % ( |
| 2873 - self._filename, timeout)) |
| 2874 + logger.warn('Could not lock %s in %s seconds', |
| 2875 + self._filename, timeout) |
| 2876 if self._fh: |
| 2877 self._fh.close() |
| 2878 self._fh = open(self._filename, self._fallback_mode) |
| 2879 @@ -267,7 +272,7 @@ try: |
| 2880 validate_file(self._filename) |
| 2881 try: |
| 2882 self._fh = open(self._filename, self._mode) |
| 2883 - except IOError, e: |
| 2884 + except IOError as e: |
| 2885 # If we can't access with _mode, try _fallback_mode and don't lock. |
| 2886 if e.errno == errno.EACCES: |
| 2887 self._fh = open(self._filename, self._fallback_mode) |
| 2888 @@ -284,9 +289,9 @@ try: |
| 2889 pywintypes.OVERLAPPED()) |
| 2890 self._locked = True |
| 2891 return |
| 2892 - except pywintypes.error, e: |
| 2893 + except pywintypes.error as e: |
| 2894 if timeout == 0: |
| 2895 - raise e |
| 2896 + raise |
| 2897 |
| 2898 # If the error is not that the file is already in use, raise. |
| 2899 if e[0] != _Win32Opener.FILE_IN_USE_ERROR: |
| 2900 @@ -308,7 +313,7 @@ try: |
| 2901 try: |
| 2902 hfile = win32file._get_osfhandle(self._fh.fileno()) |
| 2903 win32file.UnlockFileEx(hfile, 0, -0x10000, pywintypes.OVERLAPPED()) |
| 2904 - except pywintypes.error, e: |
| 2905 + except pywintypes.error as e: |
| 2906 if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR: |
| 2907 raise |
| 2908 self._locked = False |
| 2909 @@ -370,4 +375,4 @@ class LockedFile(object): |
| 2910 |
| 2911 def unlock_and_close(self): |
| 2912 """Unlock and close a file.""" |
| 2913 - self._opener.unlock_and_close() |
| 2914 + self._opener.unlock_and_close() |
| 2915 \ No newline at end of file |
| 2916 diff --git a/third_party/oauth2client/multistore_file.py b/third_party/oauth2cli
ent/multistore_file.py |
| 2917 index ea89027..5008c00 100644 |
| 2918 --- a/third_party/oauth2client/multistore_file.py |
| 2919 +++ b/third_party/oauth2client/multistore_file.py |
| 2920 @@ -1,4 +1,4 @@ |
| 2921 -# Copyright 2011 Google Inc. |
| 2922 +# Copyright 2014 Google Inc. All rights reserved. |
| 2923 # |
| 2924 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 2925 # you may not use this file except in compliance with the License. |
| 2926 @@ -19,39 +19,41 @@ credentials can be stored in one file. That file supports lo
cking |
| 2927 both in a single process and across processes. |
| 2928 |
| 2929 The credential themselves are keyed off of: |
| 2930 + |
| 2931 * client_id |
| 2932 * user_agent |
| 2933 * scope |
| 2934 |
| 2935 -The format of the stored data is like so: |
| 2936 -{ |
| 2937 - 'file_version': 1, |
| 2938 - 'data': [ |
| 2939 - { |
| 2940 - 'key': { |
| 2941 - 'clientId': '<client id>', |
| 2942 - 'userAgent': '<user agent>', |
| 2943 - 'scope': '<scope>' |
| 2944 - }, |
| 2945 - 'credential': { |
| 2946 - # JSON serialized Credentials. |
| 2947 +The format of the stored data is like so:: |
| 2948 + |
| 2949 + { |
| 2950 + 'file_version': 1, |
| 2951 + 'data': [ |
| 2952 + { |
| 2953 + 'key': { |
| 2954 + 'clientId': '<client id>', |
| 2955 + 'userAgent': '<user agent>', |
| 2956 + 'scope': '<scope>' |
| 2957 + }, |
| 2958 + 'credential': { |
| 2959 + # JSON serialized Credentials. |
| 2960 + } |
| 2961 } |
| 2962 - } |
| 2963 - ] |
| 2964 -} |
| 2965 + ] |
| 2966 + } |
| 2967 + |
| 2968 """ |
| 2969 |
| 2970 __author__ = 'jbeda@google.com (Joe Beda)' |
| 2971 |
| 2972 -import base64 |
| 2973 import errno |
| 2974 +import json |
| 2975 import logging |
| 2976 import os |
| 2977 import threading |
| 2978 |
| 2979 -from anyjson import simplejson |
| 2980 -from .client import Storage as BaseStorage |
| 2981 from .client import Credentials |
| 2982 +from .client import Storage as BaseStorage |
| 2983 from . import util |
| 2984 from locked_file import LockedFile |
| 2985 |
| 2986 @@ -64,12 +66,10 @@ _multistores_lock = threading.Lock() |
| 2987 |
| 2988 class Error(Exception): |
| 2989 """Base error for this module.""" |
| 2990 - pass |
| 2991 |
| 2992 |
| 2993 class NewerCredentialStoreError(Error): |
| 2994 - """The credential store is a newer version that supported.""" |
| 2995 - pass |
| 2996 + """The credential store is a newer version than supported.""" |
| 2997 |
| 2998 |
| 2999 @util.positional(4) |
| 3000 @@ -193,7 +193,7 @@ class _MultiStore(object): |
| 3001 |
| 3002 This will create the file if necessary. |
| 3003 """ |
| 3004 - self._file = LockedFile(filename, 'r+b', 'rb') |
| 3005 + self._file = LockedFile(filename, 'r+', 'r') |
| 3006 self._thread_lock = threading.Lock() |
| 3007 self._read_only = False |
| 3008 self._warn_on_readonly = warn_on_readonly |
| 3009 @@ -271,7 +271,7 @@ class _MultiStore(object): |
| 3010 simple version of "touch" to ensure the file has been created. |
| 3011 """ |
| 3012 if not os.path.exists(self._file.filename()): |
| 3013 - old_umask = os.umask(0177) |
| 3014 + old_umask = os.umask(0o177) |
| 3015 try: |
| 3016 open(self._file.filename(), 'a+b').close() |
| 3017 finally: |
| 3018 @@ -280,13 +280,23 @@ class _MultiStore(object): |
| 3019 def _lock(self): |
| 3020 """Lock the entire multistore.""" |
| 3021 self._thread_lock.acquire() |
| 3022 - self._file.open_and_lock() |
| 3023 + try: |
| 3024 + self._file.open_and_lock() |
| 3025 + except IOError as e: |
| 3026 + if e.errno == errno.ENOSYS: |
| 3027 + logger.warn('File system does not support locking the credentials ' |
| 3028 + 'file.') |
| 3029 + elif e.errno == errno.ENOLCK: |
| 3030 + logger.warn('File system is out of resources for writing the ' |
| 3031 + 'credentials file (is your disk full?).') |
| 3032 + else: |
| 3033 + raise |
| 3034 if not self._file.is_locked(): |
| 3035 self._read_only = True |
| 3036 if self._warn_on_readonly: |
| 3037 logger.warn('The credentials file (%s) is not writable. Opening in ' |
| 3038 'read-only mode. Any refreshed credentials will only be ' |
| 3039 - 'valid for this run.' % self._file.filename()) |
| 3040 + 'valid for this run.', self._file.filename()) |
| 3041 if os.path.getsize(self._file.filename()) == 0: |
| 3042 logger.debug('Initializing empty multistore file') |
| 3043 # The multistore is empty so write out an empty file. |
| 3044 @@ -315,7 +325,7 @@ class _MultiStore(object): |
| 3045 """ |
| 3046 assert self._thread_lock.locked() |
| 3047 self._file.file_handle().seek(0) |
| 3048 - return simplejson.load(self._file.file_handle()) |
| 3049 + return json.load(self._file.file_handle()) |
| 3050 |
| 3051 def _locked_json_write(self, data): |
| 3052 """Write a JSON serializable data structure to the multistore. |
| 3053 @@ -329,7 +339,7 @@ class _MultiStore(object): |
| 3054 if self._read_only: |
| 3055 return |
| 3056 self._file.file_handle().seek(0) |
| 3057 - simplejson.dump(data, self._file.file_handle(), sort_keys=True, indent=2) |
| 3058 + json.dump(data, self._file.file_handle(), sort_keys=True, indent=2, separat
ors=(',', ': ')) |
| 3059 self._file.file_handle().truncate() |
| 3060 |
| 3061 def _refresh_data_cache(self): |
| 3062 @@ -387,7 +397,7 @@ class _MultiStore(object): |
| 3063 raw_key = cred_entry['key'] |
| 3064 key = util.dict_to_tuple_key(raw_key) |
| 3065 credential = None |
| 3066 - credential = Credentials.new_from_json(simplejson.dumps(cred_entry['credent
ial'])) |
| 3067 + credential = Credentials.new_from_json(json.dumps(cred_entry['credential'])
) |
| 3068 return (key, credential) |
| 3069 |
| 3070 def _write(self): |
| 3071 @@ -400,7 +410,7 @@ class _MultiStore(object): |
| 3072 raw_data['data'] = raw_creds |
| 3073 for (cred_key, cred) in self._data.items(): |
| 3074 raw_key = dict(cred_key) |
| 3075 - raw_cred = simplejson.loads(cred.to_json()) |
| 3076 + raw_cred = json.loads(cred.to_json()) |
| 3077 raw_creds.append({'key': raw_key, 'credential': raw_cred}) |
| 3078 self._locked_json_write(raw_data) |
| 3079 |
| 3080 @@ -462,4 +472,4 @@ class _MultiStore(object): |
| 3081 Returns: |
| 3082 A Storage object that can be used to get/set this cred |
| 3083 """ |
| 3084 - return self._Storage(self, key) |
| 3085 + return self._Storage(self, key) |
| 3086 \ No newline at end of file |
| 3087 diff --git a/third_party/oauth2client/old_run.py b/third_party/oauth2client/old_
run.py |
| 3088 index da23358..221fff8 100644 |
| 3089 --- a/third_party/oauth2client/old_run.py |
| 3090 +++ b/third_party/oauth2client/old_run.py |
| 3091 @@ -1,4 +1,4 @@ |
| 3092 -# Copyright (C) 2013 Google Inc. |
| 3093 +# Copyright 2014 Google Inc. All rights reserved. |
| 3094 # |
| 3095 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 3096 # you may not use this file except in compliance with the License. |
| 3097 @@ -15,6 +15,7 @@ |
| 3098 """This module holds the old run() function which is deprecated, the |
| 3099 tools.run_flow() function should be used in its place.""" |
| 3100 |
| 3101 +from __future__ import print_function |
| 3102 |
| 3103 import logging |
| 3104 import socket |
| 3105 @@ -22,9 +23,9 @@ import sys |
| 3106 import webbrowser |
| 3107 |
| 3108 import gflags |
| 3109 - |
| 3110 -from oauth2client import client |
| 3111 -from oauth2client import util |
| 3112 +from third_party.six.moves import input |
| 3113 +from third_party.oauth2client import client |
| 3114 +from third_party.oauth2client import util |
| 3115 from tools import ClientRedirectHandler |
| 3116 from tools import ClientRedirectServer |
| 3117 |
| 3118 @@ -48,39 +49,38 @@ gflags.DEFINE_multi_int('auth_host_port', [8080, 8090], |
| 3119 def run(flow, storage, http=None): |
| 3120 """Core code for a command-line application. |
| 3121 |
| 3122 - The run() function is called from your application and runs through all |
| 3123 - the steps to obtain credentials. It takes a Flow argument and attempts to |
| 3124 - open an authorization server page in the user's default web browser. The |
| 3125 - server asks the user to grant your application access to the user's data. |
| 3126 - If the user grants access, the run() function returns new credentials. The |
| 3127 - new credentials are also stored in the Storage argument, which updates the |
| 3128 - file associated with the Storage object. |
| 3129 + The ``run()`` function is called from your application and runs |
| 3130 + through all the steps to obtain credentials. It takes a ``Flow`` |
| 3131 + argument and attempts to open an authorization server page in the |
| 3132 + user's default web browser. The server asks the user to grant your |
| 3133 + application access to the user's data. If the user grants access, |
| 3134 + the ``run()`` function returns new credentials. The new credentials |
| 3135 + are also stored in the ``storage`` argument, which updates the file |
| 3136 + associated with the ``Storage`` object. |
| 3137 |
| 3138 It presumes it is run from a command-line application and supports the |
| 3139 following flags: |
| 3140 |
| 3141 - --auth_host_name: Host name to use when running a local web server |
| 3142 - to handle redirects during OAuth authorization. |
| 3143 - (default: 'localhost') |
| 3144 + ``--auth_host_name`` (string, default: ``localhost``) |
| 3145 + Host name to use when running a local web server to handle |
| 3146 + redirects during OAuth authorization. |
| 3147 |
| 3148 - --auth_host_port: Port to use when running a local web server to handle |
| 3149 - redirects during OAuth authorization.; |
| 3150 - repeat this option to specify a list of values |
| 3151 - (default: '[8080, 8090]') |
| 3152 - (an integer) |
| 3153 + ``--auth_host_port`` (integer, default: ``[8080, 8090]``) |
| 3154 + Port to use when running a local web server to handle redirects |
| 3155 + during OAuth authorization. Repeat this option to specify a list |
| 3156 + of values. |
| 3157 |
| 3158 - --[no]auth_local_webserver: Run a local web server to handle redirects |
| 3159 - during OAuth authorization. |
| 3160 - (default: 'true') |
| 3161 + ``--[no]auth_local_webserver`` (boolean, default: ``True``) |
| 3162 + Run a local web server to handle redirects during OAuth authorization. |
| 3163 |
| 3164 - Since it uses flags make sure to initialize the gflags module before |
| 3165 - calling run(). |
| 3166 + Since it uses flags make sure to initialize the ``gflags`` module before |
| 3167 + calling ``run()``. |
| 3168 |
| 3169 Args: |
| 3170 flow: Flow, an OAuth 2.0 Flow to step through. |
| 3171 - storage: Storage, a Storage to store the credential in. |
| 3172 - http: An instance of httplib2.Http.request |
| 3173 - or something that acts like it. |
| 3174 + storage: Storage, a ``Storage`` to store the credential in. |
| 3175 + http: An instance of ``httplib2.Http.request`` or something that acts |
| 3176 + like it. |
| 3177 |
| 3178 Returns: |
| 3179 Credentials, the obtained credential. |
| 3180 @@ -96,20 +96,20 @@ def run(flow, storage, http=None): |
| 3181 try: |
| 3182 httpd = ClientRedirectServer((FLAGS.auth_host_name, port), |
| 3183 ClientRedirectHandler) |
| 3184 - except socket.error, e: |
| 3185 + except socket.error as e: |
| 3186 pass |
| 3187 else: |
| 3188 success = True |
| 3189 break |
| 3190 FLAGS.auth_local_webserver = success |
| 3191 if not success: |
| 3192 - print 'Failed to start a local webserver listening on either port 8080' |
| 3193 - print 'or port 9090. Please check your firewall settings and locally' |
| 3194 - print 'running programs that may be blocking or using those ports.' |
| 3195 - print |
| 3196 - print 'Falling back to --noauth_local_webserver and continuing with', |
| 3197 - print 'authorization.' |
| 3198 - print |
| 3199 + print('Failed to start a local webserver listening on either port 8080') |
| 3200 + print('or port 9090. Please check your firewall settings and locally') |
| 3201 + print('running programs that may be blocking or using those ports.') |
| 3202 + print() |
| 3203 + print('Falling back to --noauth_local_webserver and continuing with') |
| 3204 + print('authorization.') |
| 3205 + print() |
| 3206 |
| 3207 if FLAGS.auth_local_webserver: |
| 3208 oauth_callback = 'http://%s:%s/' % (FLAGS.auth_host_name, port_number) |
| 3209 @@ -120,20 +120,20 @@ def run(flow, storage, http=None): |
| 3210 |
| 3211 if FLAGS.auth_local_webserver: |
| 3212 webbrowser.open(authorize_url, new=1, autoraise=True) |
| 3213 - print 'Your browser has been opened to visit:' |
| 3214 - print |
| 3215 - print ' ' + authorize_url |
| 3216 - print |
| 3217 - print 'If your browser is on a different machine then exit and re-run' |
| 3218 - print 'this application with the command-line parameter ' |
| 3219 - print |
| 3220 - print ' --noauth_local_webserver' |
| 3221 - print |
| 3222 + print('Your browser has been opened to visit:') |
| 3223 + print() |
| 3224 + print(' ' + authorize_url) |
| 3225 + print() |
| 3226 + print('If your browser is on a different machine then exit and re-run') |
| 3227 + print('this application with the command-line parameter ') |
| 3228 + print() |
| 3229 + print(' --noauth_local_webserver') |
| 3230 + print() |
| 3231 else: |
| 3232 - print 'Go to the following link in your browser:' |
| 3233 - print |
| 3234 - print ' ' + authorize_url |
| 3235 - print |
| 3236 + print('Go to the following link in your browser:') |
| 3237 + print() |
| 3238 + print(' ' + authorize_url) |
| 3239 + print() |
| 3240 |
| 3241 code = None |
| 3242 if FLAGS.auth_local_webserver: |
| 3243 @@ -143,18 +143,18 @@ def run(flow, storage, http=None): |
| 3244 if 'code' in httpd.query_params: |
| 3245 code = httpd.query_params['code'] |
| 3246 else: |
| 3247 - print 'Failed to find "code" in the query parameters of the redirect.' |
| 3248 + print('Failed to find "code" in the query parameters of the redirect.') |
| 3249 sys.exit('Try running with --noauth_local_webserver.') |
| 3250 else: |
| 3251 - code = raw_input('Enter verification code: ').strip() |
| 3252 + code = input('Enter verification code: ').strip() |
| 3253 |
| 3254 try: |
| 3255 credential = flow.step2_exchange(code, http=http) |
| 3256 - except client.FlowExchangeError, e: |
| 3257 + except client.FlowExchangeError as e: |
| 3258 sys.exit('Authentication has failed: %s' % e) |
| 3259 |
| 3260 storage.put(credential) |
| 3261 credential.set_store(storage) |
| 3262 - print 'Authentication successful.' |
| 3263 + print('Authentication successful.') |
| 3264 |
| 3265 - return credential |
| 3266 + return credential |
| 3267 \ No newline at end of file |
| 3268 diff --git a/third_party/oauth2client/service_account.py b/third_party/oauth2cli
ent/service_account.py |
| 3269 new file mode 100644 |
| 3270 index 0000000..b6b694c |
| 3271 --- /dev/null |
| 3272 +++ b/third_party/oauth2client/service_account.py |
| 3273 @@ -0,0 +1,139 @@ |
| 3274 +# Copyright 2014 Google Inc. All rights reserved. |
| 3275 +# |
| 3276 +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 3277 +# you may not use this file except in compliance with the License. |
| 3278 +# You may obtain a copy of the License at |
| 3279 +# |
| 3280 +# http://www.apache.org/licenses/LICENSE-2.0 |
| 3281 +# |
| 3282 +# Unless required by applicable law or agreed to in writing, software |
| 3283 +# distributed under the License is distributed on an "AS IS" BASIS, |
| 3284 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 3285 +# See the License for the specific language governing permissions and |
| 3286 +# limitations under the License. |
| 3287 + |
| 3288 +"""A service account credentials class. |
| 3289 + |
| 3290 +This credentials class is implemented on top of rsa library. |
| 3291 +""" |
| 3292 + |
| 3293 +import base64 |
| 3294 +import json |
| 3295 +import time |
| 3296 + |
| 3297 +from pyasn1.codec.ber import decoder |
| 3298 +from pyasn1_modules.rfc5208 import PrivateKeyInfo |
| 3299 +import rsa |
| 3300 + |
| 3301 +from . import GOOGLE_REVOKE_URI |
| 3302 +from . import GOOGLE_TOKEN_URI |
| 3303 +from . import util |
| 3304 +from client import AssertionCredentials |
| 3305 +from third_party import six |
| 3306 + |
| 3307 + |
| 3308 +class _ServiceAccountCredentials(AssertionCredentials): |
| 3309 + """Class representing a service account (signed JWT) credential.""" |
| 3310 + |
| 3311 + MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds |
| 3312 + |
| 3313 + def __init__(self, service_account_id, service_account_email, private_key_id, |
| 3314 + private_key_pkcs8_text, scopes, user_agent=None, |
| 3315 + token_uri=GOOGLE_TOKEN_URI, revoke_uri=GOOGLE_REVOKE_URI, |
| 3316 + **kwargs): |
| 3317 + |
| 3318 + super(_ServiceAccountCredentials, self).__init__( |
| 3319 + None, user_agent=user_agent, token_uri=token_uri, revoke_uri=revoke_uri
) |
| 3320 + |
| 3321 + self._service_account_id = service_account_id |
| 3322 + self._service_account_email = service_account_email |
| 3323 + self._private_key_id = private_key_id |
| 3324 + self._private_key = _get_private_key(private_key_pkcs8_text) |
| 3325 + self._private_key_pkcs8_text = private_key_pkcs8_text |
| 3326 + self._scopes = util.scopes_to_string(scopes) |
| 3327 + self._user_agent = user_agent |
| 3328 + self._token_uri = token_uri |
| 3329 + self._revoke_uri = revoke_uri |
| 3330 + self._kwargs = kwargs |
| 3331 + |
| 3332 + def _generate_assertion(self): |
| 3333 + """Generate the assertion that will be used in the request.""" |
| 3334 + |
| 3335 + header = { |
| 3336 + 'alg': 'RS256', |
| 3337 + 'typ': 'JWT', |
| 3338 + 'kid': self._private_key_id |
| 3339 + } |
| 3340 + |
| 3341 + now = int(time.time()) |
| 3342 + payload = { |
| 3343 + 'aud': self._token_uri, |
| 3344 + 'scope': self._scopes, |
| 3345 + 'iat': now, |
| 3346 + 'exp': now + _ServiceAccountCredentials.MAX_TOKEN_LIFETIME_SECS, |
| 3347 + 'iss': self._service_account_email |
| 3348 + } |
| 3349 + payload.update(self._kwargs) |
| 3350 + |
| 3351 + assertion_input = (_urlsafe_b64encode(header) + b'.' + |
| 3352 + _urlsafe_b64encode(payload)) |
| 3353 + |
| 3354 + # Sign the assertion. |
| 3355 + rsa_bytes = rsa.pkcs1.sign(assertion_input, self._private_key, 'SHA-256') |
| 3356 + signature = base64.urlsafe_b64encode(rsa_bytes).rstrip(b'=') |
| 3357 + |
| 3358 + return assertion_input + b'.' + signature |
| 3359 + |
| 3360 + def sign_blob(self, blob): |
| 3361 + # Ensure that it is bytes |
| 3362 + try: |
| 3363 + blob = blob.encode('utf-8') |
| 3364 + except AttributeError: |
| 3365 + pass |
| 3366 + return (self._private_key_id, |
| 3367 + rsa.pkcs1.sign(blob, self._private_key, 'SHA-256')) |
| 3368 + |
| 3369 + @property |
| 3370 + def service_account_email(self): |
| 3371 + return self._service_account_email |
| 3372 + |
| 3373 + @property |
| 3374 + def serialization_data(self): |
| 3375 + return { |
| 3376 + 'type': 'service_account', |
| 3377 + 'client_id': self._service_account_id, |
| 3378 + 'client_email': self._service_account_email, |
| 3379 + 'private_key_id': self._private_key_id, |
| 3380 + 'private_key': self._private_key_pkcs8_text |
| 3381 + } |
| 3382 + |
| 3383 + def create_scoped_required(self): |
| 3384 + return not self._scopes |
| 3385 + |
| 3386 + def create_scoped(self, scopes): |
| 3387 + return _ServiceAccountCredentials(self._service_account_id, |
| 3388 + self._service_account_email, |
| 3389 + self._private_key_id, |
| 3390 + self._private_key_pkcs8_text, |
| 3391 + scopes, |
| 3392 + user_agent=self._user_agent, |
| 3393 + token_uri=self._token_uri, |
| 3394 + revoke_uri=self._revoke_uri, |
| 3395 + **self._kwargs) |
| 3396 + |
| 3397 + |
| 3398 +def _urlsafe_b64encode(data): |
| 3399 + return base64.urlsafe_b64encode( |
| 3400 + json.dumps(data, separators=(',', ':')).encode('UTF-8')).rstrip(b'=') |
| 3401 + |
| 3402 + |
| 3403 +def _get_private_key(private_key_pkcs8_text): |
| 3404 + """Get an RSA private key object from a pkcs8 representation.""" |
| 3405 + |
| 3406 + if not isinstance(private_key_pkcs8_text, six.binary_type): |
| 3407 + private_key_pkcs8_text = private_key_pkcs8_text.encode('ascii') |
| 3408 + der = rsa.pem.load_pem(private_key_pkcs8_text, 'PRIVATE KEY') |
| 3409 + asn1_private_key, _ = decoder.decode(der, asn1Spec=PrivateKeyInfo()) |
| 3410 + return rsa.PrivateKey.load_pkcs1( |
| 3411 + asn1_private_key.getComponentByName('privateKey').asOctets(), |
| 3412 + format='DER') |
| 3413 \ No newline at end of file |
| 3414 diff --git a/third_party/oauth2client/tools.py b/third_party/oauth2client/tools.
py |
| 3415 index 12c68bf..3df1bbe 100644 |
| 3416 --- a/third_party/oauth2client/tools.py |
| 3417 +++ b/third_party/oauth2client/tools.py |
| 3418 @@ -1,4 +1,4 @@ |
| 3419 -# Copyright (C) 2013 Google Inc. |
| 3420 +# Copyright 2014 Google Inc. All rights reserved. |
| 3421 # |
| 3422 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 3423 # you may not use this file except in compliance with the License. |
| 3424 @@ -19,27 +19,22 @@ generated credentials in a common file that is used by other
example apps in |
| 3425 the same directory. |
| 3426 """ |
| 3427 |
| 3428 +from __future__ import print_function |
| 3429 + |
| 3430 __author__ = 'jcgregorio@google.com (Joe Gregorio)' |
| 3431 __all__ = ['argparser', 'run_flow', 'run', 'message_if_missing'] |
| 3432 |
| 3433 - |
| 3434 -import BaseHTTPServer |
| 3435 -import argparse |
| 3436 -import httplib2 |
| 3437 import logging |
| 3438 -import os |
| 3439 import socket |
| 3440 import sys |
| 3441 -import webbrowser |
| 3442 |
| 3443 -from oauth2client import client |
| 3444 -from oauth2client import file |
| 3445 -from oauth2client import util |
| 3446 +from third_party.six.moves import BaseHTTPServer |
| 3447 +from third_party.six.moves import urllib |
| 3448 +from third_party.six.moves import input |
| 3449 + |
| 3450 +from . import client |
| 3451 +from . import util |
| 3452 |
| 3453 -try: |
| 3454 - from urlparse import parse_qsl |
| 3455 -except ImportError: |
| 3456 - from cgi import parse_qsl |
| 3457 |
| 3458 _CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0 |
| 3459 |
| 3460 @@ -52,20 +47,27 @@ with information from the APIs Console <https://code.google.
com/apis/console>. |
| 3461 |
| 3462 """ |
| 3463 |
| 3464 -# run_parser is an ArgumentParser that contains command-line options expected |
| 3465 +def _CreateArgumentParser(): |
| 3466 + try: |
| 3467 + import argparse |
| 3468 + except ImportError: |
| 3469 + return None |
| 3470 + parser = argparse.ArgumentParser(add_help=False) |
| 3471 + parser.add_argument('--auth_host_name', default='localhost', |
| 3472 + help='Hostname when running a local web server.') |
| 3473 + parser.add_argument('--noauth_local_webserver', action='store_true', |
| 3474 + default=False, help='Do not run a local web server.') |
| 3475 + parser.add_argument('--auth_host_port', default=[8080, 8090], type=int, |
| 3476 + nargs='*', help='Port web server should listen on.') |
| 3477 + parser.add_argument('--logging_level', default='ERROR', |
| 3478 + choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
, |
| 3479 + help='Set the logging level of detail.') |
| 3480 + return parser |
| 3481 + |
| 3482 +# argparser is an ArgumentParser that contains command-line options expected |
| 3483 # by tools.run(). Pass it in as part of the 'parents' argument to your own |
| 3484 # ArgumentParser. |
| 3485 -argparser = argparse.ArgumentParser(add_help=False) |
| 3486 -argparser.add_argument('--auth_host_name', default='localhost', |
| 3487 - help='Hostname when running a local web server.') |
| 3488 -argparser.add_argument('--noauth_local_webserver', action='store_true', |
| 3489 - default=False, help='Do not run a local web server.') |
| 3490 -argparser.add_argument('--auth_host_port', default=[8080, 8090], type=int, |
| 3491 - nargs='*', help='Port web server should listen on.') |
| 3492 -argparser.add_argument('--logging_level', default='ERROR', |
| 3493 - choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', |
| 3494 - 'CRITICAL'], |
| 3495 - help='Set the logging level of detail.') |
| 3496 +argparser = _CreateArgumentParser() |
| 3497 |
| 3498 |
| 3499 class ClientRedirectServer(BaseHTTPServer.HTTPServer): |
| 3500 @@ -84,72 +86,75 @@ class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHa
ndler): |
| 3501 into the servers query_params and then stops serving. |
| 3502 """ |
| 3503 |
| 3504 - def do_GET(s): |
| 3505 + def do_GET(self): |
| 3506 """Handle a GET request. |
| 3507 |
| 3508 Parses the query parameters and prints a message |
| 3509 if the flow has completed. Note that we can't detect |
| 3510 if an error occurred. |
| 3511 """ |
| 3512 - s.send_response(200) |
| 3513 - s.send_header("Content-type", "text/html") |
| 3514 - s.end_headers() |
| 3515 - query = s.path.split('?', 1)[-1] |
| 3516 - query = dict(parse_qsl(query)) |
| 3517 - s.server.query_params = query |
| 3518 - s.wfile.write("<html><head><title>Authentication Status</title></head>") |
| 3519 - s.wfile.write("<body><p>The authentication flow has completed.</p>") |
| 3520 - s.wfile.write("</body></html>") |
| 3521 + self.send_response(200) |
| 3522 + self.send_header("Content-type", "text/html") |
| 3523 + self.end_headers() |
| 3524 + query = self.path.split('?', 1)[-1] |
| 3525 + query = dict(urllib.parse.parse_qsl(query)) |
| 3526 + self.server.query_params = query |
| 3527 + self.wfile.write(b"<html><head><title>Authentication Status</title></head>"
) |
| 3528 + self.wfile.write(b"<body><p>The authentication flow has completed.</p>") |
| 3529 + self.wfile.write(b"</body></html>") |
| 3530 |
| 3531 def log_message(self, format, *args): |
| 3532 """Do not log messages to stdout while running as command line program.""" |
| 3533 - pass |
| 3534 |
| 3535 |
| 3536 @util.positional(3) |
| 3537 def run_flow(flow, storage, flags, http=None): |
| 3538 """Core code for a command-line application. |
| 3539 |
| 3540 - The run() function is called from your application and runs through all the |
| 3541 - steps to obtain credentials. It takes a Flow argument and attempts to open an |
| 3542 - authorization server page in the user's default web browser. The server asks |
| 3543 - the user to grant your application access to the user's data. If the user |
| 3544 - grants access, the run() function returns new credentials. The new credential
s |
| 3545 - are also stored in the Storage argument, which updates the file associated |
| 3546 - with the Storage object. |
| 3547 + The ``run()`` function is called from your application and runs |
| 3548 + through all the steps to obtain credentials. It takes a ``Flow`` |
| 3549 + argument and attempts to open an authorization server page in the |
| 3550 + user's default web browser. The server asks the user to grant your |
| 3551 + application access to the user's data. If the user grants access, |
| 3552 + the ``run()`` function returns new credentials. The new credentials |
| 3553 + are also stored in the ``storage`` argument, which updates the file |
| 3554 + associated with the ``Storage`` object. |
| 3555 |
| 3556 It presumes it is run from a command-line application and supports the |
| 3557 following flags: |
| 3558 |
| 3559 - --auth_host_name: Host name to use when running a local web server |
| 3560 - to handle redirects during OAuth authorization. |
| 3561 - (default: 'localhost') |
| 3562 + ``--auth_host_name`` (string, default: ``localhost``) |
| 3563 + Host name to use when running a local web server to handle |
| 3564 + redirects during OAuth authorization. |
| 3565 + |
| 3566 + ``--auth_host_port`` (integer, default: ``[8080, 8090]``) |
| 3567 + Port to use when running a local web server to handle redirects |
| 3568 + during OAuth authorization. Repeat this option to specify a list |
| 3569 + of values. |
| 3570 + |
| 3571 + ``--[no]auth_local_webserver`` (boolean, default: ``True``) |
| 3572 + Run a local web server to handle redirects during OAuth authorization. |
| 3573 + |
| 3574 |
| 3575 - --auth_host_port: Port to use when running a local web server to handle |
| 3576 - redirects during OAuth authorization.; |
| 3577 - repeat this option to specify a list of values |
| 3578 - (default: '[8080, 8090]') |
| 3579 - (an integer) |
| 3580 |
| 3581 - --[no]auth_local_webserver: Run a local web server to handle redirects |
| 3582 - during OAuth authorization. |
| 3583 - (default: 'true') |
| 3584 |
| 3585 - The tools module defines an ArgumentParser the already contains the flag |
| 3586 - definitions that run() requires. You can pass that ArgumentParser to your |
| 3587 - ArgumentParser constructor: |
| 3588 + The tools module defines an ``ArgumentParser`` the already contains the flag |
| 3589 + definitions that ``run()`` requires. You can pass that ``ArgumentParser`` to
your |
| 3590 + ``ArgumentParser`` constructor:: |
| 3591 |
| 3592 - parser = argparse.ArgumentParser(description=__doc__, |
| 3593 - formatter_class=argparse.RawDescriptionHelpFormatter, |
| 3594 - parents=[tools.run_parser]) |
| 3595 - flags = parser.parse_args(argv) |
| 3596 + parser = argparse.ArgumentParser(description=__doc__, |
| 3597 + formatter_class=argparse.RawDescriptionHelpFormatter, |
| 3598 + parents=[tools.argparser]) |
| 3599 + flags = parser.parse_args(argv) |
| 3600 |
| 3601 Args: |
| 3602 flow: Flow, an OAuth 2.0 Flow to step through. |
| 3603 - storage: Storage, a Storage to store the credential in. |
| 3604 - flags: argparse.ArgumentParser, the command-line flags. |
| 3605 - http: An instance of httplib2.Http.request |
| 3606 - or something that acts like it. |
| 3607 + storage: Storage, a ``Storage`` to store the credential in. |
| 3608 + flags: ``argparse.Namespace``, The command-line flags. This is the |
| 3609 + object returned from calling ``parse_args()`` on |
| 3610 + ``argparse.ArgumentParser`` as described above. |
| 3611 + http: An instance of ``httplib2.Http.request`` or something that |
| 3612 + acts like it. |
| 3613 |
| 3614 Returns: |
| 3615 Credentials, the obtained credential. |
| 3616 @@ -163,20 +168,20 @@ def run_flow(flow, storage, flags, http=None): |
| 3617 try: |
| 3618 httpd = ClientRedirectServer((flags.auth_host_name, port), |
| 3619 ClientRedirectHandler) |
| 3620 - except socket.error, e: |
| 3621 + except socket.error: |
| 3622 pass |
| 3623 else: |
| 3624 success = True |
| 3625 break |
| 3626 flags.noauth_local_webserver = not success |
| 3627 if not success: |
| 3628 - print 'Failed to start a local webserver listening on either port 8080' |
| 3629 - print 'or port 9090. Please check your firewall settings and locally' |
| 3630 - print 'running programs that may be blocking or using those ports.' |
| 3631 - print |
| 3632 - print 'Falling back to --noauth_local_webserver and continuing with', |
| 3633 - print 'authorization.' |
| 3634 - print |
| 3635 + print('Failed to start a local webserver listening on either port 8080') |
| 3636 + print('or port 9090. Please check your firewall settings and locally') |
| 3637 + print('running programs that may be blocking or using those ports.') |
| 3638 + print() |
| 3639 + print('Falling back to --noauth_local_webserver and continuing with') |
| 3640 + print('authorization.') |
| 3641 + print() |
| 3642 |
| 3643 if not flags.noauth_local_webserver: |
| 3644 oauth_callback = 'http://%s:%s/' % (flags.auth_host_name, port_number) |
| 3645 @@ -186,21 +191,22 @@ def run_flow(flow, storage, flags, http=None): |
| 3646 authorize_url = flow.step1_get_authorize_url() |
| 3647 |
| 3648 if not flags.noauth_local_webserver: |
| 3649 + import webbrowser |
| 3650 webbrowser.open(authorize_url, new=1, autoraise=True) |
| 3651 - print 'Your browser has been opened to visit:' |
| 3652 - print |
| 3653 - print ' ' + authorize_url |
| 3654 - print |
| 3655 - print 'If your browser is on a different machine then exit and re-run this' |
| 3656 - print 'application with the command-line parameter ' |
| 3657 - print |
| 3658 - print ' --noauth_local_webserver' |
| 3659 - print |
| 3660 + print('Your browser has been opened to visit:') |
| 3661 + print() |
| 3662 + print(' ' + authorize_url) |
| 3663 + print() |
| 3664 + print('If your browser is on a different machine then exit and re-run this'
) |
| 3665 + print('application with the command-line parameter ') |
| 3666 + print() |
| 3667 + print(' --noauth_local_webserver') |
| 3668 + print() |
| 3669 else: |
| 3670 - print 'Go to the following link in your browser:' |
| 3671 - print |
| 3672 - print ' ' + authorize_url |
| 3673 - print |
| 3674 + print('Go to the following link in your browser:') |
| 3675 + print() |
| 3676 + print(' ' + authorize_url) |
| 3677 + print() |
| 3678 |
| 3679 code = None |
| 3680 if not flags.noauth_local_webserver: |
| 3681 @@ -210,19 +216,19 @@ def run_flow(flow, storage, flags, http=None): |
| 3682 if 'code' in httpd.query_params: |
| 3683 code = httpd.query_params['code'] |
| 3684 else: |
| 3685 - print 'Failed to find "code" in the query parameters of the redirect.' |
| 3686 + print('Failed to find "code" in the query parameters of the redirect.') |
| 3687 sys.exit('Try running with --noauth_local_webserver.') |
| 3688 else: |
| 3689 - code = raw_input('Enter verification code: ').strip() |
| 3690 + code = input('Enter verification code: ').strip() |
| 3691 |
| 3692 try: |
| 3693 credential = flow.step2_exchange(code, http=http) |
| 3694 - except client.FlowExchangeError, e: |
| 3695 + except client.FlowExchangeError as e: |
| 3696 sys.exit('Authentication has failed: %s' % e) |
| 3697 |
| 3698 storage.put(credential) |
| 3699 credential.set_store(storage) |
| 3700 - print 'Authentication successful.' |
| 3701 + print('Authentication successful.') |
| 3702 |
| 3703 return credential |
| 3704 |
| 3705 @@ -233,11 +239,11 @@ def message_if_missing(filename): |
| 3706 return _CLIENT_SECRETS_MESSAGE % filename |
| 3707 |
| 3708 try: |
| 3709 - from old_run import run |
| 3710 - from old_run import FLAGS |
| 3711 + from oauth2client.old_run import run |
| 3712 + from oauth2client.old_run import FLAGS |
| 3713 except ImportError: |
| 3714 def run(*args, **kwargs): |
| 3715 raise NotImplementedError( |
| 3716 'The gflags library must be installed to use tools.run(). ' |
| 3717 'Please install gflags or preferrably switch to using ' |
| 3718 - 'tools.run_flow().') |
| 3719 + 'tools.run_flow().') |
| 3720 \ No newline at end of file |
| 3721 diff --git a/third_party/oauth2client/util.py b/third_party/oauth2client/util.py |
| 3722 index 90dff15..b0963ef 100644 |
| 3723 --- a/third_party/oauth2client/util.py |
| 3724 +++ b/third_party/oauth2client/util.py |
| 3725 @@ -1,6 +1,6 @@ |
| 3726 #!/usr/bin/env python |
| 3727 # |
| 3728 -# Copyright 2010 Google Inc. |
| 3729 +# Copyright 2014 Google Inc. All rights reserved. |
| 3730 # |
| 3731 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 3732 # you may not use this file except in compliance with the License. |
| 3733 @@ -17,26 +17,27 @@ |
| 3734 |
| 3735 """Common utility library.""" |
| 3736 |
| 3737 -__author__ = ['rafek@google.com (Rafe Kaplan)', |
| 3738 - 'guido@google.com (Guido van Rossum)', |
| 3739 +__author__ = [ |
| 3740 + 'rafek@google.com (Rafe Kaplan)', |
| 3741 + 'guido@google.com (Guido van Rossum)', |
| 3742 ] |
| 3743 + |
| 3744 __all__ = [ |
| 3745 - 'positional', |
| 3746 - 'POSITIONAL_WARNING', |
| 3747 - 'POSITIONAL_EXCEPTION', |
| 3748 - 'POSITIONAL_IGNORE', |
| 3749 + 'positional', |
| 3750 + 'POSITIONAL_WARNING', |
| 3751 + 'POSITIONAL_EXCEPTION', |
| 3752 + 'POSITIONAL_IGNORE', |
| 3753 ] |
| 3754 |
| 3755 +import functools |
| 3756 import inspect |
| 3757 import logging |
| 3758 +import sys |
| 3759 import types |
| 3760 -import urllib |
| 3761 -import urlparse |
| 3762 |
| 3763 -try: |
| 3764 - from urlparse import parse_qsl |
| 3765 -except ImportError: |
| 3766 - from cgi import parse_qsl |
| 3767 +from third_party import six |
| 3768 +from third_party.six.moves import urllib |
| 3769 + |
| 3770 |
| 3771 logger = logging.getLogger(__name__) |
| 3772 |
| 3773 @@ -51,56 +52,58 @@ positional_parameters_enforcement = POSITIONAL_WARNING |
| 3774 def positional(max_positional_args): |
| 3775 """A decorator to declare that only the first N arguments my be positional. |
| 3776 |
| 3777 - This decorator makes it easy to support Python 3 style key-word only |
| 3778 - parameters. For example, in Python 3 it is possible to write: |
| 3779 + This decorator makes it easy to support Python 3 style keyword-only |
| 3780 + parameters. For example, in Python 3 it is possible to write:: |
| 3781 |
| 3782 def fn(pos1, *, kwonly1=None, kwonly1=None): |
| 3783 ... |
| 3784 |
| 3785 - All named parameters after * must be a keyword: |
| 3786 + All named parameters after ``*`` must be a keyword:: |
| 3787 |
| 3788 fn(10, 'kw1', 'kw2') # Raises exception. |
| 3789 fn(10, kwonly1='kw1') # Ok. |
| 3790 |
| 3791 - Example: |
| 3792 - To define a function like above, do: |
| 3793 + Example |
| 3794 + ^^^^^^^ |
| 3795 |
| 3796 - @positional(1) |
| 3797 - def fn(pos1, kwonly1=None, kwonly2=None): |
| 3798 - ... |
| 3799 + To define a function like above, do:: |
| 3800 |
| 3801 - If no default value is provided to a keyword argument, it becomes a require
d |
| 3802 - keyword argument: |
| 3803 + @positional(1) |
| 3804 + def fn(pos1, kwonly1=None, kwonly2=None): |
| 3805 + ... |
| 3806 |
| 3807 - @positional(0) |
| 3808 - def fn(required_kw): |
| 3809 - ... |
| 3810 + If no default value is provided to a keyword argument, it becomes a required |
| 3811 + keyword argument:: |
| 3812 |
| 3813 - This must be called with the keyword parameter: |
| 3814 + @positional(0) |
| 3815 + def fn(required_kw): |
| 3816 + ... |
| 3817 |
| 3818 - fn() # Raises exception. |
| 3819 - fn(10) # Raises exception. |
| 3820 - fn(required_kw=10) # Ok. |
| 3821 + This must be called with the keyword parameter:: |
| 3822 |
| 3823 - When defining instance or class methods always remember to account for |
| 3824 - 'self' and 'cls': |
| 3825 + fn() # Raises exception. |
| 3826 + fn(10) # Raises exception. |
| 3827 + fn(required_kw=10) # Ok. |
| 3828 |
| 3829 - class MyClass(object): |
| 3830 + When defining instance or class methods always remember to account for |
| 3831 + ``self`` and ``cls``:: |
| 3832 |
| 3833 - @positional(2) |
| 3834 - def my_method(self, pos1, kwonly1=None): |
| 3835 - ... |
| 3836 + class MyClass(object): |
| 3837 |
| 3838 - @classmethod |
| 3839 - @positional(2) |
| 3840 - def my_method(cls, pos1, kwonly1=None): |
| 3841 - ... |
| 3842 + @positional(2) |
| 3843 + def my_method(self, pos1, kwonly1=None): |
| 3844 + ... |
| 3845 + |
| 3846 + @classmethod |
| 3847 + @positional(2) |
| 3848 + def my_method(cls, pos1, kwonly1=None): |
| 3849 + ... |
| 3850 |
| 3851 The positional decorator behavior is controlled by |
| 3852 - util.positional_parameters_enforcement, which may be set to |
| 3853 - POSITIONAL_EXCEPTION, POSITIONAL_WARNING or POSITIONAL_IGNORE to raise an |
| 3854 - exception, log a warning, or do nothing, respectively, if a declaration is |
| 3855 - violated. |
| 3856 + ``util.positional_parameters_enforcement``, which may be set to |
| 3857 + ``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or |
| 3858 + ``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do |
| 3859 + nothing, respectively, if a declaration is violated. |
| 3860 |
| 3861 Args: |
| 3862 max_positional_arguments: Maximum number of positional arguments. All |
| 3863 @@ -114,8 +117,10 @@ def positional(max_positional_args): |
| 3864 TypeError if a key-word only argument is provided as a positional |
| 3865 parameter, but only if util.positional_parameters_enforcement is set to |
| 3866 POSITIONAL_EXCEPTION. |
| 3867 + |
| 3868 """ |
| 3869 def positional_decorator(wrapped): |
| 3870 + @functools.wraps(wrapped) |
| 3871 def positional_wrapper(*args, **kwargs): |
| 3872 if len(args) > max_positional_args: |
| 3873 plural_s = '' |
| 3874 @@ -132,7 +137,7 @@ def positional(max_positional_args): |
| 3875 return wrapped(*args, **kwargs) |
| 3876 return positional_wrapper |
| 3877 |
| 3878 - if isinstance(max_positional_args, (int, long)): |
| 3879 + if isinstance(max_positional_args, six.integer_types): |
| 3880 return positional_decorator |
| 3881 else: |
| 3882 args, _, _, defaults = inspect.getargspec(max_positional_args) |
| 3883 @@ -152,7 +157,7 @@ def scopes_to_string(scopes): |
| 3884 Returns: |
| 3885 The scopes formatted as a single string. |
| 3886 """ |
| 3887 - if isinstance(scopes, types.StringTypes): |
| 3888 + if isinstance(scopes, six.string_types): |
| 3889 return scopes |
| 3890 else: |
| 3891 return ' '.join(scopes) |
| 3892 @@ -189,8 +194,8 @@ def _add_query_parameter(url, name, value): |
| 3893 if value is None: |
| 3894 return url |
| 3895 else: |
| 3896 - parsed = list(urlparse.urlparse(url)) |
| 3897 - q = dict(parse_qsl(parsed[4])) |
| 3898 + parsed = list(urllib.parse.urlparse(url)) |
| 3899 + q = dict(urllib.parse.parse_qsl(parsed[4])) |
| 3900 q[name] = value |
| 3901 - parsed[4] = urllib.urlencode(q) |
| 3902 - return urlparse.urlunparse(parsed) |
| 3903 + parsed[4] = urllib.parse.urlencode(q) |
| 3904 + return urllib.parse.urlunparse(parsed) |
| 3905 \ No newline at end of file |
| 3906 diff --git a/third_party/oauth2client/xsrfutil.py b/third_party/oauth2client/xsr
futil.py |
| 3907 index 7e1fe5c..bea7c87 100644 |
| 3908 --- a/third_party/oauth2client/xsrfutil.py |
| 3909 +++ b/third_party/oauth2client/xsrfutil.py |
| 3910 @@ -1,6 +1,5 @@ |
| 3911 -#!/usr/bin/python2.5 |
| 3912 # |
| 3913 -# Copyright 2010 the Melange authors. |
| 3914 +# Copyright 2014 the Melange authors. |
| 3915 # |
| 3916 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 3917 # you may not use this file except in compliance with the License. |
| 3918 @@ -17,25 +16,36 @@ |
| 3919 """Helper methods for creating & verifying XSRF tokens.""" |
| 3920 |
| 3921 __authors__ = [ |
| 3922 - '"Doug Coker" <dcoker@google.com>', |
| 3923 - '"Joe Gregorio" <jcgregorio@google.com>', |
| 3924 + '"Doug Coker" <dcoker@google.com>', |
| 3925 + '"Joe Gregorio" <jcgregorio@google.com>', |
| 3926 ] |
| 3927 |
| 3928 |
| 3929 import base64 |
| 3930 import hmac |
| 3931 -import os # for urandom |
| 3932 import time |
| 3933 |
| 3934 +import six |
| 3935 from oauth2client import util |
| 3936 |
| 3937 |
| 3938 # Delimiter character |
| 3939 -DELIMITER = ':' |
| 3940 +DELIMITER = b':' |
| 3941 + |
| 3942 |
| 3943 # 1 hour in seconds |
| 3944 DEFAULT_TIMEOUT_SECS = 1*60*60 |
| 3945 |
| 3946 + |
| 3947 +def _force_bytes(s): |
| 3948 + if isinstance(s, bytes): |
| 3949 + return s |
| 3950 + s = str(s) |
| 3951 + if isinstance(s, six.text_type): |
| 3952 + return s.encode('utf-8') |
| 3953 + return s |
| 3954 + |
| 3955 + |
| 3956 @util.positional(2) |
| 3957 def generate_token(key, user_id, action_id="", when=None): |
| 3958 """Generates a URL-safe token for the given user, action, time tuple. |
| 3959 @@ -51,18 +61,16 @@ def generate_token(key, user_id, action_id="", when=None): |
| 3960 Returns: |
| 3961 A string XSRF protection token. |
| 3962 """ |
| 3963 - when = when or int(time.time()) |
| 3964 - digester = hmac.new(key) |
| 3965 - digester.update(str(user_id)) |
| 3966 + when = _force_bytes(when or int(time.time())) |
| 3967 + digester = hmac.new(_force_bytes(key)) |
| 3968 + digester.update(_force_bytes(user_id)) |
| 3969 digester.update(DELIMITER) |
| 3970 - digester.update(action_id) |
| 3971 + digester.update(_force_bytes(action_id)) |
| 3972 digester.update(DELIMITER) |
| 3973 - digester.update(str(when)) |
| 3974 + digester.update(when) |
| 3975 digest = digester.digest() |
| 3976 |
| 3977 - token = base64.urlsafe_b64encode('%s%s%d' % (digest, |
| 3978 - DELIMITER, |
| 3979 - when)) |
| 3980 + token = base64.urlsafe_b64encode(digest + DELIMITER + when) |
| 3981 return token |
| 3982 |
| 3983 |
| 3984 @@ -87,8 +95,8 @@ def validate_token(key, token, user_id, action_id="", current_
time=None): |
| 3985 if not token: |
| 3986 return False |
| 3987 try: |
| 3988 - decoded = base64.urlsafe_b64decode(str(token)) |
| 3989 - token_time = long(decoded.split(DELIMITER)[-1]) |
| 3990 + decoded = base64.urlsafe_b64decode(token) |
| 3991 + token_time = int(decoded.split(DELIMITER)[-1]) |
| 3992 except (TypeError, ValueError): |
| 3993 return False |
| 3994 if current_time is None: |
| 3995 @@ -105,9 +113,6 @@ def validate_token(key, token, user_id, action_id="", curren
t_time=None): |
| 3996 |
| 3997 # Perform constant time comparison to avoid timing attacks |
| 3998 different = 0 |
| 3999 - for x, y in zip(token, expected_token): |
| 4000 - different |= ord(x) ^ ord(y) |
| 4001 - if different: |
| 4002 - return False |
| 4003 - |
| 4004 - return True |
| 4005 + for x, y in zip(bytearray(token), bytearray(expected_token)): |
| 4006 + different |= x ^ y |
| 4007 + return not different |
| 4008 \ No newline at end of file |
OLD | NEW |