From 8f8bf75ceb5b1fd32af95c2cd18b3e17733ecf47 Mon Sep 17 00:00:00 2001 From: Kevin Cheng Date: Fri, 8 Feb 2019 21:17:48 +0000 Subject: Revert "Upgrade oauth2client to v4.1.3" This reverts commit 5acb77a57ddaaf4b5e5dff31a36faf5fd3388d91. Reason for revert: broke acloud b/112803893 Change-Id: I60a8e5bcdd1d69fa9cf04ebb01d515a1b833dc1f --- .coveragerc | 4 - .github/ISSUE_TEMPLATE.md | 3 - .github/PULL_REQUEST_TEMPLATE.md | 3 - .gitignore | 1 - .travis.yml | 39 +- CHANGELOG.md | 79 -- CODE_OF_CONDUCT.md | 43 - CONTRIBUTING.md | 6 + CONTRIBUTORS.md | 95 -- LICENSE | 210 +--- MANIFEST.in | 4 +- METADATA | 12 +- README.md | 6 +- docs/index.rst | 18 +- docs/source/oauth2client.client.rst | 4 +- docs/source/oauth2client.clientsecrets.rst | 4 +- docs/source/oauth2client.contrib.appengine.rst | 4 +- docs/source/oauth2client.contrib.devshell.rst | 4 +- .../oauth2client.contrib.dictionary_storage.rst | 4 +- .../oauth2client.contrib.django_util.apps.rst | 4 +- ...oauth2client.contrib.django_util.decorators.rst | 4 +- .../oauth2client.contrib.django_util.models.rst | 4 +- docs/source/oauth2client.contrib.django_util.rst | 4 +- .../oauth2client.contrib.django_util.signals.rst | 4 +- .../oauth2client.contrib.django_util.site.rst | 4 +- .../oauth2client.contrib.django_util.storage.rst | 4 +- .../oauth2client.contrib.django_util.views.rst | 4 +- docs/source/oauth2client.contrib.flask_util.rst | 4 +- docs/source/oauth2client.contrib.gce.rst | 4 +- .../oauth2client.contrib.keyring_storage.rst | 4 +- docs/source/oauth2client.contrib.locked_file.rst | 7 + ...th2client.contrib.multiprocess_file_storage.rst | 4 +- .../oauth2client.contrib.multistore_file.rst | 7 + docs/source/oauth2client.contrib.rst | 6 +- docs/source/oauth2client.contrib.sqlalchemy.rst | 4 +- docs/source/oauth2client.contrib.xsrfutil.rst | 4 +- docs/source/oauth2client.crypt.rst | 4 +- docs/source/oauth2client.file.rst | 4 +- docs/source/oauth2client.rst | 1 + docs/source/oauth2client.service_account.rst | 4 +- docs/source/oauth2client.tools.rst | 4 +- docs/source/oauth2client.transport.rst | 4 +- docs/source/oauth2client.util.rst | 7 + oauth2client/__init__.py | 11 +- oauth2client/_helpers.py | 236 ----- oauth2client/_pkce.py | 67 -- oauth2client/client.py | 277 +++-- oauth2client/clientsecrets.py | 1 + oauth2client/contrib/_fcntl_opener.py | 81 ++ oauth2client/contrib/_metadata.py | 45 +- oauth2client/contrib/_win32_opener.py | 106 ++ oauth2client/contrib/appengine.py | 33 +- oauth2client/contrib/devshell.py | 8 +- oauth2client/contrib/django_util/__init__.py | 36 +- oauth2client/contrib/django_util/models.py | 11 +- oauth2client/contrib/django_util/views.py | 21 +- oauth2client/contrib/flask_util.py | 9 +- oauth2client/contrib/gce.py | 32 +- oauth2client/contrib/keyring_storage.py | 3 + oauth2client/contrib/locked_file.py | 234 ++++ oauth2client/contrib/multistore_file.py | 505 +++++++++ oauth2client/contrib/xsrfutil.py | 9 +- oauth2client/file.py | 21 +- oauth2client/service_account.py | 18 +- oauth2client/tools.py | 18 +- oauth2client/transport.py | 74 +- oauth2client/util.py | 206 ++++ samples/django/README.md | 21 - samples/django/django_user/manage.py | 23 - samples/django/django_user/myoauth/__init__.py | 0 samples/django/django_user/myoauth/settings.py | 115 -- samples/django/django_user/myoauth/urls.py | 30 - samples/django/django_user/myoauth/wsgi.py | 21 - samples/django/django_user/polls/__init__.py | 0 samples/django/django_user/polls/models.py | 23 - .../polls/templates/registration/login.html | 45 - samples/django/django_user/polls/views.py | 41 - samples/django/django_user/requirements.txt | 3 - samples/django/google_user/manage.py | 23 - samples/django/google_user/myoauth/__init__.py | 0 samples/django/google_user/myoauth/settings.py | 107 -- samples/django/google_user/myoauth/urls.py | 26 - samples/django/google_user/myoauth/wsgi.py | 21 - samples/django/google_user/polls/__init__.py | 0 samples/django/google_user/polls/views.py | 41 - samples/django/google_user/requirements.txt | 3 - scripts/fetch_gae_sdk.py | 85 ++ scripts/install.sh | 22 +- scripts/run.sh | 9 +- scripts/run_gce_system_tests.py | 26 +- scripts/run_system_tests.py | 14 +- scripts/run_system_tests.sh | 11 +- setup.cfg | 2 - setup.py | 37 +- tests/__init__.py | 22 + tests/conftest.py | 31 - tests/contrib/appengine/__init__.py | 0 tests/contrib/appengine/conftest.py | 53 - tests/contrib/appengine/test__appengine_ndb.py | 166 --- tests/contrib/appengine/test_appengine.py | 1120 -------------------- tests/contrib/django_util/test_decorators.py | 37 +- tests/contrib/django_util/test_django_models.py | 44 +- tests/contrib/django_util/test_django_storage.py | 4 +- tests/contrib/django_util/test_django_util.py | 41 +- tests/contrib/django_util/test_views.py | 102 +- tests/contrib/test__appengine_ndb.py | 166 +++ tests/contrib/test_appengine.py | 1073 +++++++++++++++++++ tests/contrib/test_devshell.py | 26 +- tests/contrib/test_dictionary_storage.py | 4 +- tests/contrib/test_flask_util.py | 94 +- tests/contrib/test_gce.py | 69 +- tests/contrib/test_keyring_storage.py | 17 +- tests/contrib/test_locked_file.py | 244 +++++ tests/contrib/test_metadata.py | 83 +- tests/contrib/test_multiprocess_file_storage.py | 71 +- tests/contrib/test_multistore_file.py | 383 +++++++ tests/contrib/test_sqlalchemy.py | 30 +- tests/contrib/test_xsrfutil.py | 27 +- tests/data/client_secrets.json | 4 +- tests/data/unfilled_client_secrets.json | 2 +- tests/data/user-key.json.enc | Bin 256 -> 240 bytes tests/http_mock.py | 44 +- tests/test__helpers.py | 190 +--- tests/test__pkce.py | 54 - tests/test__pure_python_crypt.py | 6 +- tests/test__pycrypto_crypt.py | 7 +- tests/test_client.py | 627 ++++++----- tests/test_clientsecrets.py | 14 +- tests/test_crypt.py | 62 +- tests/test_file.py | 171 ++- tests/test_jwt.py | 85 +- tests/test_service_account.py | 226 ++-- tests/test_tools.py | 14 +- tests/test_transport.py | 67 +- tests/test_util.py | 122 +++ tox.ini | 112 +- 136 files changed, 4574 insertions(+), 4598 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 CODE_OF_CONDUCT.md delete mode 100644 CONTRIBUTORS.md create mode 100644 docs/source/oauth2client.contrib.locked_file.rst create mode 100644 docs/source/oauth2client.contrib.multistore_file.rst create mode 100644 docs/source/oauth2client.util.rst delete mode 100644 oauth2client/_pkce.py create mode 100644 oauth2client/contrib/_fcntl_opener.py create mode 100644 oauth2client/contrib/_win32_opener.py create mode 100644 oauth2client/contrib/locked_file.py create mode 100644 oauth2client/contrib/multistore_file.py create mode 100644 oauth2client/util.py delete mode 100644 samples/django/README.md delete mode 100755 samples/django/django_user/manage.py delete mode 100644 samples/django/django_user/myoauth/__init__.py delete mode 100644 samples/django/django_user/myoauth/settings.py delete mode 100644 samples/django/django_user/myoauth/urls.py delete mode 100644 samples/django/django_user/myoauth/wsgi.py delete mode 100644 samples/django/django_user/polls/__init__.py delete mode 100644 samples/django/django_user/polls/models.py delete mode 100644 samples/django/django_user/polls/templates/registration/login.html delete mode 100644 samples/django/django_user/polls/views.py delete mode 100644 samples/django/django_user/requirements.txt delete mode 100755 samples/django/google_user/manage.py delete mode 100644 samples/django/google_user/myoauth/__init__.py delete mode 100644 samples/django/google_user/myoauth/settings.py delete mode 100644 samples/django/google_user/myoauth/urls.py delete mode 100644 samples/django/google_user/myoauth/wsgi.py delete mode 100644 samples/django/google_user/polls/__init__.py delete mode 100644 samples/django/google_user/polls/views.py delete mode 100644 samples/django/google_user/requirements.txt create mode 100755 scripts/fetch_gae_sdk.py delete mode 100644 setup.cfg delete mode 100644 tests/conftest.py delete mode 100644 tests/contrib/appengine/__init__.py delete mode 100644 tests/contrib/appengine/conftest.py delete mode 100644 tests/contrib/appengine/test__appengine_ndb.py delete mode 100644 tests/contrib/appengine/test_appengine.py create mode 100644 tests/contrib/test__appengine_ndb.py create mode 100644 tests/contrib/test_appengine.py create mode 100644 tests/contrib/test_locked_file.py create mode 100644 tests/contrib/test_multistore_file.py delete mode 100644 tests/test__pkce.py create mode 100644 tests/test_util.py diff --git a/.coveragerc b/.coveragerc index 3a3e2cd..0151e07 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,10 +1,6 @@ -[run] -branch = True - [report] omit = */samples/* - */conftest.py # Don't report coverage over platform-specific modules. oauth2client/contrib/_fcntl_opener.py oauth2client/contrib/_win32_opener.py diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 2ce3395..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,3 +0,0 @@ -**Note**: oauth2client is now deprecated. As such, it is unlikely that we will -address or respond to your issue. We recommend you use -[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/). diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 1fbd4d2..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,3 +0,0 @@ -**Note**: oauth2client is now deprecated. As such, it is unlikely that we will -review or merge to your pull request. We recommend you use -[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/). diff --git a/.gitignore b/.gitignore index 0bc898c..89c1121 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ docs/_build # Test files .tox/ -.cache/ # Django test database db.sqlite3 diff --git a/.travis.yml b/.travis.yml index 47570be..a8c01fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,46 +1,41 @@ language: python +python: 2.7 sudo: false - +# TODO(issue 532): Fix syntax when 3.5 is natively available upstream matrix: include: - - python: 2.7 - env: TOX_ENV=flake8 - - python: 2.7 - env: TOX_ENV=docs - - python: 2.7 - env: TOX_ENV=gae - - python: 2.7 - env: TOX_ENV=py27 - - python: 3.4 - env: TOX_ENV=py34 - - python: 3.5 - env: TOX_ENV=py35 - - python: 2.7 - env: TOX_ENV=system-tests - - python: 3.4 - env: TOX_ENV=system-tests3 - - python: 2.7 - env: TOX_ENV=cover + - python: 3.5 + env: + - TOX_ENV=py35 env: + matrix: + - TOX_ENV=py26 + - TOX_ENV=py27 + - TOX_ENV=py33 + - TOX_ENV=py34 + - TOX_ENV=pypy + - TOX_ENV=docs + - TOX_ENV=system-tests + - TOX_ENV=system-tests3 + - TOX_ENV=gae + - TOX_ENV=flake8 global: - GAE_PYTHONPATH=${HOME}/.cache/google_appengine cache: directories: - ${HOME}/.cache - - ${HOME}/.pyenv install: - ./scripts/install.sh script: - ./scripts/run.sh after_success: -- if [[ "${TOX_ENV}" == "cover" ]]; then coveralls; fi +- if [[ "${TOX_ENV}" == "gae" ]]; then tox -e coveralls; fi notifications: email: false deploy: provider: pypi user: gcloudpypi - distributions: sdist bdist_wheel password: secure: "C9ImNa5kbdnrQNfX9ww4PUtQIr3tN+nfxl7eDkP1B8Qr0QNYjrjov7x+DLImkKvmoJd3dxYtYIpLE9esObUHu0gKHYxqymNHtuAAyoBOUfPtmp0vIEse9brNKMtaey5Ngk7ZWz9EHKBBqRHxqgN+Giby+K9Ta3K3urJIq6urYhE=" on: diff --git a/CHANGELOG.md b/CHANGELOG.md index 2523d29..bf33dea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,84 +1,5 @@ # CHANGELOG -## v4.1.3 - -**Note**: oauth2client is deprecated. No more features will be added to the -libraries and the core team is turning down support. We recommend you use -[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/). - -* Changed OAuth2 endpoints to use oauth2.googleapis.com variants. (#742) - -## v4.1.2 - -**Note**: oauth2client is deprecated. No more features will be added to the -libraries and the core team is turning down support. We recommend you use -[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/). - -Bug fixes: -* Fix packaging issue had erroneously installed the test package. (#688) - -## v4.1.1 - -**Note**: oauth2client is deprecated. No more features will be added to the -libraries and the core team is turning down support. We recommend you use -[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/). - -New features: -* Allow passing prompt='consent' via the flow_from_clientsecrets. (#717) - -## v4.1.0 - -**Note**: oauth2client is now deprecated. No more features will be added to the -libraries and the core team is turning down support. We recommend you use -[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/). - -New features: -* Allow customizing the GCE metadata service address via an env var. (#704) -* Store original encoded and signed identity JWT in OAuth2Credentials. (#680) -* Use jsonpickle in django contrib, if available. (#676) - -Bug fixes: -* Typo fixes. (#668, #697) -* Remove b64 padding from PKCE values, per RFC7636. (#683) -* Include LICENSE in Manifest.in. (#694) -* Fix tests and CI. (#705, #712, #713) -* Escape callback error code in flask_util. (#710) - -## v4.0.0 - -New features: -* New Django samples. (#636) -* Add support for RFC7636 PKCE. (#588) -* Release as a universal wheel. (#665) - -Bug fixes: -* Fix django authorization redirect by correctly checking validity of credentials. (#651) -* Correct query loss when using parse_qsl to dict. (#622) -* Switch django models from pickle to jsonpickle. (#614) -* Support new MIDDLEWARE Django 1.10 setting. (#623) -* Remove usage of os.environ.setdefault. (#621) -* Handle missing storage files correctly. (#576) -* Try to revoke token with POST when getting a 405. (#662) - -Internal changes: -* Use transport module for GCE environment check. (#612) -* Remove __author__ lines and add contributors.md. (#627) -* Clean up imports. (#625) -* Use transport.request in tests. (#607) -* Drop unittest2 dependency (#610) -* Remove backslash line continuations. (#608) -* Use transport helpers in system tests. (#606) -* Clean up usage of HTTP mocks in tests. (#605) -* Remove all uses of MagicMock. (#598) -* Migrate test runner to pytest. (#569) -* Merge util.py and _helpers.py. (#579) -* Remove httplib2 imports from non-transport modules. (#577) - -Breaking changes: -* Drop Python 3.3 support. (#603) -* Drop Python 2.6 support. (#590) -* Remove multistore_file. (#589) - ## v3.0.0 * Populate `token_expiry` for GCE credentials. (#473) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 46b2a08..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,43 +0,0 @@ -# Contributor Code of Conduct - -As contributors and maintainers of this project, -and in the interest of fostering an open and welcoming community, -we pledge to respect all people who contribute through reporting issues, -posting feature requests, updating documentation, -submitting pull requests or patches, and other activities. - -We are committed to making participation in this project -a harassment-free experience for everyone, -regardless of level of experience, gender, gender identity and expression, -sexual orientation, disability, personal appearance, -body size, race, ethnicity, age, religion, or nationality. - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery -* Personal attacks -* Trolling or insulting/derogatory comments -* Public or private harassment -* Publishing other's private information, -such as physical or electronic -addresses, without explicit permission -* Other unethical or unprofessional conduct. - -Project maintainers have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct. -By adopting this Code of Conduct, -project maintainers commit themselves to fairly and consistently -applying these principles to every aspect of managing this project. -Project maintainers who do not follow or enforce the Code of Conduct -may be permanently removed from the project team. - -This code of conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior -may be reported by opening an issue -or contacting one or more of the project maintainers. - -This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, -available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 990c534..15b9455 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -127,6 +127,10 @@ Running Tests least version 2.6 of `pypy` installed. See the [docs][13] for more information. +- **Note** that `django` related tests are turned off for Python 2.6 + and 3.3. This is because `django` dropped support for + [2.6 in `django==1.7`][14] and for [3.3 in `django==1.9`][15]. + Running System Tests -------------------- @@ -198,5 +202,7 @@ we'll be able to accept your pull requests. [11]: #include-tests [12]: #make-the-pull-request [13]: https://oauth2client.readthedocs.io/en/latest/#using-pypy +[14]: https://docs.djangoproject.com/en/1.7/faq/install/#what-python-version-can-i-use-with-django +[15]: https://docs.djangoproject.com/en/1.9/faq/install/#what-python-version-can-i-use-with-django [GooglePythonStyle]: https://google.github.io/styleguide/pyguide.html [GitCommitRules]: http://chris.beams.io/posts/git-commit/#seven-rules diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md deleted file mode 100644 index 00bd09f..0000000 --- a/CONTRIBUTORS.md +++ /dev/null @@ -1,95 +0,0 @@ -# Contribors to oauth2client - -## Maintainers - -* [Nathaniel Manista](https://github.com/nathanielmanistaatgoogle) -* [Jon Wayne Parrott](https://github.com/jonparrott) -* [Danny Hermes](https://github.com/dhermes) - -Previous maintainers: - -* [Craig Citro](https://github.com/craigcitro) -* [Joe Gregorio](https://github.com/jcgregorio) - -## Contributors - -This list is generated from git commit authors. - -* aalexand -* Aaron -* Adam Chainz -* ade@google.com -* Alexandre Vivien -* Ali Afshar -* Andrzej Pragacz -* api.nickm@gmail.com -* Ben Demaree -* Bill Prin -* Brendan McCollam -* Craig Citro -* Dan Ring -* Daniel Hermes -* Danilo Akamine -* daryl herzmann -* dlorenc -* Dominik Miedziński -* dr. Kertész Csaba-Zoltán -* Dustin Farris -* Eddie Warner -* Edwin Amsler -* elibixby -* Emanuele Pucciarelli -* Eric Koleda -* Frederik Creemers -* Guido van Rossum -* Harsh Vardhan -* Herr Kaste -* INADA Naoki -* JacobMoshenko -* Jay Lee -* Jed Hartman -* Jeff Terrace -* Jeffrey Sorensen -* Jeremi Joslin -* Jin Liu -* Joe Beda -* Joe Gregorio -* Johan Euphrosine -* John Asmuth -* John Vandenberg -* Jon Wayne Parrott -* Jose Alcerreca -* KCs -* Keith Maxwell -* Ken Payson -* Kevin Regan -* lraccomando -* Luar Roji -* Luke Blanshard -* Marc Cohen -* Mark Pellegrini -* Martin Trigaux -* Matt McDonald -* Nathan Naze -* Nathaniel Manista -* Orest Bolohan -* Pat Ferate -* Patrick Costello -* Rafe Kaplan -* rahulpaul@google.com -* RM Saksida -* Robert Kaplow -* Robert Spies -* Sergei Trofimovich -* sgomes@google.com -* Simon Cadman -* soltanmm -* Sébastien de Melo -* takuya sato -* thobrla -* Tom Miller -* Tony Aiuto -* Travis Hobrla -* Veres Lajos -* Vivek Seth -* Éamonn McManus diff --git a/LICENSE b/LICENSE index c8d76df..b506d50 100644 --- a/LICENSE +++ b/LICENSE @@ -1,205 +1,16 @@ + Copyright 2014 Google Inc. - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + http://www.apache.org/licenses/LICENSE-2.0 - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. Dependent Modules ================= @@ -207,4 +18,5 @@ Dependent Modules This code has the following dependencies above and beyond the Python standard library: +uritemplates - Apache License 2.0 httplib2 - MIT License diff --git a/MANIFEST.in b/MANIFEST.in index 4f2ba45..39f5637 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -include README.md LICENSE CHANGELOG.md -recursive-include tests * +include README.md +recursive-exclude tests * diff --git a/METADATA b/METADATA index 97c5852..6a258aa 100644 --- a/METADATA +++ b/METADATA @@ -1,5 +1,7 @@ name: "oauth2client" -description: "This is a client library for accessing resources protected by OAuth 2.0." +description: + "This is a client library for accessing resources protected by OAuth 2.0." + third_party { url { type: HOMEPAGE @@ -9,10 +11,6 @@ third_party { type: GIT value: "https://github.com/google/oauth2client" } - version: "v4.1.3" - last_upgrade_date { - year: 2019 - month: 2 - day: 1 - } + version: "v3.0.0" + last_upgrade_date { year: 2018 month: 6 day: 6 } } diff --git a/README.md b/README.md index 5e7aade..17e69fc 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,6 @@ This is a client library for accessing resources protected by OAuth 2.0. -**Note**: oauth2client is now deprecated. No more features will be added to the -libraries and the core team is turning down support. We recommend you use -[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/). For more details on the deprecation, see [oauth2client deprecation](https://google-auth.readthedocs.io/en/latest/oauth2client-deprecation.html). - Installation ============ @@ -27,7 +23,7 @@ agreement. Supported Python Versions ========================= -We support Python 2.7 and 3.4+. More information [in the docs][2]. +We support Python 2.6, 2.7, 3.3+. More information [in the docs][2]. [1]: https://github.com/google/oauth2client/blob/master/CONTRIBUTING.md [2]: https://oauth2client.readthedocs.io/#supported-python-versions diff --git a/docs/index.rst b/docs/index.rst index 4b9f38a..0543e1a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,14 +1,6 @@ oauth2client ============ -.. note:: oauth2client is now deprecated. No more features will be added to the -libraries and the core team is turning down support. We recommend you use -`google-auth`_ and `oauthlib`_. For more details on the deprecation, see `oauth2client deprecation`_. - -.. _google-auth: https://google-auth.readthedocs.io -.. _oauthlib: http://oauthlib.readthedocs.io/ -.. _oauth2client deprecation: https://google-auth.readthedocs.io/en/latest/oauth2client-deprecation.html - *making OAuth2 just a little less painful* ``oauth2client`` makes it easy to interact with OAuth2-protected resources, @@ -115,24 +107,18 @@ contributor license agreement. Supported Python Versions ------------------------- -We support Python 2.7 and 3.4+. (Whatever this file says, the truth is +We support Python 2.6, 2.7, 3.3+. (Whatever this file says, the truth is always represented by our `tox.ini`_). .. _tox.ini: https://github.com/google/oauth2client/blob/master/tox.ini We explicitly decided to support Python 3 beginning with version -3.4. Reasons for this include: +3.3. Reasons for this include: * Encouraging use of newest versions of Python 3 * Following the lead of prominent `open-source projects`_ * Unicode literal support which allows for a cleaner codebase that works in both Python 2 and Python 3 -* Prominent projects like `django`_ have `dropped support`_ for earlier - versions (3.3 support dropped in December 2015, and 2.6 support - `dropped`_ in September 2014) .. _open-source projects: http://docs.python-requests.org/en/latest/ .. _Unicode literal support: https://www.python.org/dev/peps/pep-0414/ -.. _django: https://docs.djangoproject.com/ -.. _dropped support: https://docs.djangoproject.com/en/1.9/faq/install/#what-python-version-can-i-use-with-django -.. _dropped: https://docs.djangoproject.com/en/1.7/faq/install/#what-python-version-can-i-use-with-django diff --git a/docs/source/oauth2client.client.rst b/docs/source/oauth2client.client.rst index edb2f97..f3b1832 100644 --- a/docs/source/oauth2client.client.rst +++ b/docs/source/oauth2client.client.rst @@ -1,5 +1,5 @@ -oauth2client\.client module -=========================== +oauth2client.client module +========================== .. automodule:: oauth2client.client :members: diff --git a/docs/source/oauth2client.clientsecrets.rst b/docs/source/oauth2client.clientsecrets.rst index a839444..d666564 100644 --- a/docs/source/oauth2client.clientsecrets.rst +++ b/docs/source/oauth2client.clientsecrets.rst @@ -1,5 +1,5 @@ -oauth2client\.clientsecrets module -================================== +oauth2client.clientsecrets module +================================= .. automodule:: oauth2client.clientsecrets :members: diff --git a/docs/source/oauth2client.contrib.appengine.rst b/docs/source/oauth2client.contrib.appengine.rst index 5051495..7f3d5e2 100644 --- a/docs/source/oauth2client.contrib.appengine.rst +++ b/docs/source/oauth2client.contrib.appengine.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.appengine module -======================================= +oauth2client.contrib.appengine module +===================================== .. automodule:: oauth2client.contrib.appengine :members: diff --git a/docs/source/oauth2client.contrib.devshell.rst b/docs/source/oauth2client.contrib.devshell.rst index 66691bd..20d5c41 100644 --- a/docs/source/oauth2client.contrib.devshell.rst +++ b/docs/source/oauth2client.contrib.devshell.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.devshell module -====================================== +oauth2client.contrib.devshell module +==================================== .. automodule:: oauth2client.contrib.devshell :members: diff --git a/docs/source/oauth2client.contrib.dictionary_storage.rst b/docs/source/oauth2client.contrib.dictionary_storage.rst index b94a079..1b59a2c 100644 --- a/docs/source/oauth2client.contrib.dictionary_storage.rst +++ b/docs/source/oauth2client.contrib.dictionary_storage.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.dictionary\_storage module -================================================= +oauth2client.contrib.dictionary_storage module +============================================== .. automodule:: oauth2client.contrib.dictionary_storage :members: diff --git a/docs/source/oauth2client.contrib.django_util.apps.rst b/docs/source/oauth2client.contrib.django_util.apps.rst index 1ffe1af..b7c91ae 100644 --- a/docs/source/oauth2client.contrib.django_util.apps.rst +++ b/docs/source/oauth2client.contrib.django_util.apps.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.django\_util\.apps module -================================================ +oauth2client.contrib.django_util.apps module +============================================ .. automodule:: oauth2client.contrib.django_util.apps :members: diff --git a/docs/source/oauth2client.contrib.django_util.decorators.rst b/docs/source/oauth2client.contrib.django_util.decorators.rst index 2eb9dcf..07350bc 100644 --- a/docs/source/oauth2client.contrib.django_util.decorators.rst +++ b/docs/source/oauth2client.contrib.django_util.decorators.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.django\_util\.decorators module -====================================================== +oauth2client.contrib.django_util.decorators module +================================================== .. automodule:: oauth2client.contrib.django_util.decorators :members: diff --git a/docs/source/oauth2client.contrib.django_util.models.rst b/docs/source/oauth2client.contrib.django_util.models.rst index 91d3b8d..4be59d3 100644 --- a/docs/source/oauth2client.contrib.django_util.models.rst +++ b/docs/source/oauth2client.contrib.django_util.models.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.django\_util\.models module -================================================== +oauth2client.contrib.django_util.models module +============================================== .. automodule:: oauth2client.contrib.django_util.models :members: diff --git a/docs/source/oauth2client.contrib.django_util.rst b/docs/source/oauth2client.contrib.django_util.rst index 8247134..f60195a 100644 --- a/docs/source/oauth2client.contrib.django_util.rst +++ b/docs/source/oauth2client.contrib.django_util.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.django\_util package -=========================================== +oauth2client.contrib.django_util package +======================================== Submodules ---------- diff --git a/docs/source/oauth2client.contrib.django_util.signals.rst b/docs/source/oauth2client.contrib.django_util.signals.rst index 9a18252..70b5d2d 100644 --- a/docs/source/oauth2client.contrib.django_util.signals.rst +++ b/docs/source/oauth2client.contrib.django_util.signals.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.django\_util\.signals module -=================================================== +oauth2client.contrib.django_util.signals module +=============================================== .. automodule:: oauth2client.contrib.django_util.signals :members: diff --git a/docs/source/oauth2client.contrib.django_util.site.rst b/docs/source/oauth2client.contrib.django_util.site.rst index 5f5dae0..a271b98 100644 --- a/docs/source/oauth2client.contrib.django_util.site.rst +++ b/docs/source/oauth2client.contrib.django_util.site.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.django\_util\.site module -================================================ +oauth2client.contrib.django_util.site module +============================================ .. automodule:: oauth2client.contrib.django_util.site :members: diff --git a/docs/source/oauth2client.contrib.django_util.storage.rst b/docs/source/oauth2client.contrib.django_util.storage.rst index 4340a4c..393e738 100644 --- a/docs/source/oauth2client.contrib.django_util.storage.rst +++ b/docs/source/oauth2client.contrib.django_util.storage.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.django\_util\.storage module -=================================================== +oauth2client.contrib.django_util.storage module +=============================================== .. automodule:: oauth2client.contrib.django_util.storage :members: diff --git a/docs/source/oauth2client.contrib.django_util.views.rst b/docs/source/oauth2client.contrib.django_util.views.rst index dfba37f..4cbbea0 100644 --- a/docs/source/oauth2client.contrib.django_util.views.rst +++ b/docs/source/oauth2client.contrib.django_util.views.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.django\_util\.views module -================================================= +oauth2client.contrib.django_util.views module +============================================= .. automodule:: oauth2client.contrib.django_util.views :members: diff --git a/docs/source/oauth2client.contrib.flask_util.rst b/docs/source/oauth2client.contrib.flask_util.rst index c11c9ba..8ff2355 100644 --- a/docs/source/oauth2client.contrib.flask_util.rst +++ b/docs/source/oauth2client.contrib.flask_util.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.flask\_util module -========================================= +oauth2client.contrib.flask_util module +====================================== .. automodule:: oauth2client.contrib.flask_util :members: diff --git a/docs/source/oauth2client.contrib.gce.rst b/docs/source/oauth2client.contrib.gce.rst index d0b7a15..a3748b6 100644 --- a/docs/source/oauth2client.contrib.gce.rst +++ b/docs/source/oauth2client.contrib.gce.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.gce module -================================= +oauth2client.contrib.gce module +=============================== .. automodule:: oauth2client.contrib.gce :members: diff --git a/docs/source/oauth2client.contrib.keyring_storage.rst b/docs/source/oauth2client.contrib.keyring_storage.rst index 286e84a..0fd7476 100644 --- a/docs/source/oauth2client.contrib.keyring_storage.rst +++ b/docs/source/oauth2client.contrib.keyring_storage.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.keyring\_storage module -============================================== +oauth2client.contrib.keyring_storage module +=========================================== .. automodule:: oauth2client.contrib.keyring_storage :members: diff --git a/docs/source/oauth2client.contrib.locked_file.rst b/docs/source/oauth2client.contrib.locked_file.rst new file mode 100644 index 0000000..1076e29 --- /dev/null +++ b/docs/source/oauth2client.contrib.locked_file.rst @@ -0,0 +1,7 @@ +oauth2client.contrib.locked_file module +======================================= + +.. automodule:: oauth2client.contrib.locked_file + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/oauth2client.contrib.multiprocess_file_storage.rst b/docs/source/oauth2client.contrib.multiprocess_file_storage.rst index eb6c0c0..6f683a0 100644 --- a/docs/source/oauth2client.contrib.multiprocess_file_storage.rst +++ b/docs/source/oauth2client.contrib.multiprocess_file_storage.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.multiprocess\_file\_storage module -========================================================= +oauth2client.contrib.multiprocess_file_storage module +===================================================== .. automodule:: oauth2client.contrib.multiprocess_file_storage :members: diff --git a/docs/source/oauth2client.contrib.multistore_file.rst b/docs/source/oauth2client.contrib.multistore_file.rst new file mode 100644 index 0000000..2787b10 --- /dev/null +++ b/docs/source/oauth2client.contrib.multistore_file.rst @@ -0,0 +1,7 @@ +oauth2client.contrib.multistore_file module +=========================================== + +.. automodule:: oauth2client.contrib.multistore_file + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/oauth2client.contrib.rst b/docs/source/oauth2client.contrib.rst index 644278d..44be6f9 100644 --- a/docs/source/oauth2client.contrib.rst +++ b/docs/source/oauth2client.contrib.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib package -============================= +oauth2client.contrib package +============================ Subpackages ----------- @@ -19,7 +19,9 @@ Submodules oauth2client.contrib.flask_util oauth2client.contrib.gce oauth2client.contrib.keyring_storage + oauth2client.contrib.locked_file oauth2client.contrib.multiprocess_file_storage + oauth2client.contrib.multistore_file oauth2client.contrib.sqlalchemy oauth2client.contrib.xsrfutil diff --git a/docs/source/oauth2client.contrib.sqlalchemy.rst b/docs/source/oauth2client.contrib.sqlalchemy.rst index c4a634e..94eeeec 100644 --- a/docs/source/oauth2client.contrib.sqlalchemy.rst +++ b/docs/source/oauth2client.contrib.sqlalchemy.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.sqlalchemy module -======================================== +oauth2client.contrib.sqlalchemy module +====================================== .. automodule:: oauth2client.contrib.sqlalchemy :members: diff --git a/docs/source/oauth2client.contrib.xsrfutil.rst b/docs/source/oauth2client.contrib.xsrfutil.rst index eec497d..dd5e8d6 100644 --- a/docs/source/oauth2client.contrib.xsrfutil.rst +++ b/docs/source/oauth2client.contrib.xsrfutil.rst @@ -1,5 +1,5 @@ -oauth2client\.contrib\.xsrfutil module -====================================== +oauth2client.contrib.xsrfutil module +==================================== .. automodule:: oauth2client.contrib.xsrfutil :members: diff --git a/docs/source/oauth2client.crypt.rst b/docs/source/oauth2client.crypt.rst index d03cf50..c3b6acc 100644 --- a/docs/source/oauth2client.crypt.rst +++ b/docs/source/oauth2client.crypt.rst @@ -1,5 +1,5 @@ -oauth2client\.crypt module -========================== +oauth2client.crypt module +========================= .. automodule:: oauth2client.crypt :members: diff --git a/docs/source/oauth2client.file.rst b/docs/source/oauth2client.file.rst index bf804ff..52a9e94 100644 --- a/docs/source/oauth2client.file.rst +++ b/docs/source/oauth2client.file.rst @@ -1,5 +1,5 @@ -oauth2client\.file module -========================= +oauth2client.file module +======================== .. automodule:: oauth2client.file :members: diff --git a/docs/source/oauth2client.rst b/docs/source/oauth2client.rst index 11f64e4..65de8ac 100644 --- a/docs/source/oauth2client.rst +++ b/docs/source/oauth2client.rst @@ -20,6 +20,7 @@ Submodules oauth2client.service_account oauth2client.tools oauth2client.transport + oauth2client.util Module contents --------------- diff --git a/docs/source/oauth2client.service_account.rst b/docs/source/oauth2client.service_account.rst index c370246..0d3b382 100644 --- a/docs/source/oauth2client.service_account.rst +++ b/docs/source/oauth2client.service_account.rst @@ -1,5 +1,5 @@ -oauth2client\.service\_account module -===================================== +oauth2client.service_account module +=================================== .. automodule:: oauth2client.service_account :members: diff --git a/docs/source/oauth2client.tools.rst b/docs/source/oauth2client.tools.rst index 86be326..240ad52 100644 --- a/docs/source/oauth2client.tools.rst +++ b/docs/source/oauth2client.tools.rst @@ -1,5 +1,5 @@ -oauth2client\.tools module -========================== +oauth2client.tools module +========================= .. automodule:: oauth2client.tools :members: diff --git a/docs/source/oauth2client.transport.rst b/docs/source/oauth2client.transport.rst index c5440f1..1c6dbb0 100644 --- a/docs/source/oauth2client.transport.rst +++ b/docs/source/oauth2client.transport.rst @@ -1,5 +1,5 @@ -oauth2client\.transport module -============================== +oauth2client.transport module +============================= .. automodule:: oauth2client.transport :members: diff --git a/docs/source/oauth2client.util.rst b/docs/source/oauth2client.util.rst new file mode 100644 index 0000000..21dc8c8 --- /dev/null +++ b/docs/source/oauth2client.util.rst @@ -0,0 +1,7 @@ +oauth2client.util module +======================== + +.. automodule:: oauth2client.util + :members: + :undoc-members: + :show-inheritance: diff --git a/oauth2client/__init__.py b/oauth2client/__init__.py index 92bc191..28384bb 100644 --- a/oauth2client/__init__.py +++ b/oauth2client/__init__.py @@ -14,11 +14,10 @@ """Client library for using OAuth2, especially with Google APIs.""" -__version__ = '4.1.3' +__version__ = '3.0.0' GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth' -GOOGLE_DEVICE_URI = 'https://oauth2.googleapis.com/device/code' -GOOGLE_REVOKE_URI = 'https://oauth2.googleapis.com/revoke' -GOOGLE_TOKEN_URI = 'https://oauth2.googleapis.com/token' -GOOGLE_TOKEN_INFO_URI = 'https://oauth2.googleapis.com/tokeninfo' - +GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code' +GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke' +GOOGLE_TOKEN_URI = 'https://www.googleapis.com/oauth2/v4/token' +GOOGLE_TOKEN_INFO_URI = 'https://www.googleapis.com/oauth2/v3/tokeninfo' diff --git a/oauth2client/_helpers.py b/oauth2client/_helpers.py index e912397..cb959c5 100644 --- a/oauth2client/_helpers.py +++ b/oauth2client/_helpers.py @@ -11,248 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Helper functions for commonly used utilities.""" import base64 -import functools -import inspect import json -import logging -import os -import warnings import six -from six.moves import urllib - - -logger = logging.getLogger(__name__) - -POSITIONAL_WARNING = 'WARNING' -POSITIONAL_EXCEPTION = 'EXCEPTION' -POSITIONAL_IGNORE = 'IGNORE' -POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION, - POSITIONAL_IGNORE]) - -positional_parameters_enforcement = POSITIONAL_WARNING - -_SYM_LINK_MESSAGE = 'File: {0}: Is a symbolic link.' -_IS_DIR_MESSAGE = '{0}: Is a directory' -_MISSING_FILE_MESSAGE = 'Cannot access {0}: No such file or directory' - - -def positional(max_positional_args): - """A decorator to declare that only the first N arguments my be positional. - - This decorator makes it easy to support Python 3 style keyword-only - parameters. For example, in Python 3 it is possible to write:: - - def fn(pos1, *, kwonly1=None, kwonly1=None): - ... - - All named parameters after ``*`` must be a keyword:: - - fn(10, 'kw1', 'kw2') # Raises exception. - fn(10, kwonly1='kw1') # Ok. - - Example - ^^^^^^^ - - To define a function like above, do:: - - @positional(1) - def fn(pos1, kwonly1=None, kwonly2=None): - ... - - If no default value is provided to a keyword argument, it becomes a - required keyword argument:: - - @positional(0) - def fn(required_kw): - ... - - This must be called with the keyword parameter:: - - fn() # Raises exception. - fn(10) # Raises exception. - fn(required_kw=10) # Ok. - - When defining instance or class methods always remember to account for - ``self`` and ``cls``:: - - class MyClass(object): - - @positional(2) - def my_method(self, pos1, kwonly1=None): - ... - - @classmethod - @positional(2) - def my_method(cls, pos1, kwonly1=None): - ... - - The positional decorator behavior is controlled by - ``_helpers.positional_parameters_enforcement``, which may be set to - ``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or - ``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do - nothing, respectively, if a declaration is violated. - - Args: - max_positional_arguments: Maximum number of positional arguments. All - parameters after the this index must be - keyword only. - - Returns: - A decorator that prevents using arguments after max_positional_args - from being used as positional parameters. - - Raises: - TypeError: if a key-word only argument is provided as a positional - parameter, but only if - _helpers.positional_parameters_enforcement is set to - POSITIONAL_EXCEPTION. - """ - - def positional_decorator(wrapped): - @functools.wraps(wrapped) - def positional_wrapper(*args, **kwargs): - if len(args) > max_positional_args: - plural_s = '' - if max_positional_args != 1: - plural_s = 's' - message = ('{function}() takes at most {args_max} positional ' - 'argument{plural} ({args_given} given)'.format( - function=wrapped.__name__, - args_max=max_positional_args, - args_given=len(args), - plural=plural_s)) - if positional_parameters_enforcement == POSITIONAL_EXCEPTION: - raise TypeError(message) - elif positional_parameters_enforcement == POSITIONAL_WARNING: - logger.warning(message) - return wrapped(*args, **kwargs) - return positional_wrapper - - if isinstance(max_positional_args, six.integer_types): - return positional_decorator - else: - args, _, _, defaults = inspect.getargspec(max_positional_args) - return positional(len(args) - len(defaults))(max_positional_args) - - -def scopes_to_string(scopes): - """Converts scope value to a string. - - If scopes is a string then it is simply passed through. If scopes is an - iterable then a string is returned that is all the individual scopes - concatenated with spaces. - - Args: - scopes: string or iterable of strings, the scopes. - - Returns: - The scopes formatted as a single string. - """ - if isinstance(scopes, six.string_types): - return scopes - else: - return ' '.join(scopes) - - -def string_to_scopes(scopes): - """Converts stringifed scope value to a list. - - If scopes is a list then it is simply passed through. If scopes is an - string then a list of each individual scope is returned. - - Args: - scopes: a string or iterable of strings, the scopes. - - Returns: - The scopes in a list. - """ - if not scopes: - return [] - elif isinstance(scopes, six.string_types): - return scopes.split(' ') - else: - return scopes - - -def parse_unique_urlencoded(content): - """Parses unique key-value parameters from urlencoded content. - - Args: - content: string, URL-encoded key-value pairs. - - Returns: - dict, The key-value pairs from ``content``. - - Raises: - ValueError: if one of the keys is repeated. - """ - urlencoded_params = urllib.parse.parse_qs(content) - params = {} - for key, value in six.iteritems(urlencoded_params): - if len(value) != 1: - msg = ('URL-encoded content contains a repeated value:' - '%s -> %s' % (key, ', '.join(value))) - raise ValueError(msg) - params[key] = value[0] - return params - - -def update_query_params(uri, params): - """Updates a URI with new query parameters. - - If a given key from ``params`` is repeated in the ``uri``, then - the URI will be considered invalid and an error will occur. - - If the URI is valid, then each value from ``params`` will - replace the corresponding value in the query parameters (if - it exists). - - Args: - uri: string, A valid URI, with potential existing query parameters. - params: dict, A dictionary of query parameters. - - Returns: - The same URI but with the new query parameters added. - """ - parts = urllib.parse.urlparse(uri) - query_params = parse_unique_urlencoded(parts.query) - query_params.update(params) - new_query = urllib.parse.urlencode(query_params) - new_parts = parts._replace(query=new_query) - return urllib.parse.urlunparse(new_parts) - - -def _add_query_parameter(url, name, value): - """Adds a query parameter to a url. - - Replaces the current value if it already exists in the URL. - - Args: - url: string, url to add the query parameter to. - name: string, query parameter name. - value: string, query parameter value. - - Returns: - Updated query parameter. Does not update the url if value is None. - """ - if value is None: - return url - else: - return update_query_params(url, {name: value}) - - -def validate_file(filename): - if os.path.islink(filename): - raise IOError(_SYM_LINK_MESSAGE.format(filename)) - elif os.path.isdir(filename): - raise IOError(_IS_DIR_MESSAGE.format(filename)) - elif not os.path.isfile(filename): - warnings.warn(_MISSING_FILE_MESSAGE.format(filename)) def _parse_pem_key(raw_key_input): diff --git a/oauth2client/_pkce.py b/oauth2client/_pkce.py deleted file mode 100644 index e4952d8..0000000 --- a/oauth2client/_pkce.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Utility functions for implementing Proof Key for Code Exchange (PKCE) by OAuth -Public Clients - -See RFC7636. -""" - -import base64 -import hashlib -import os - - -def code_verifier(n_bytes=64): - """ - Generates a 'code_verifier' as described in section 4.1 of RFC 7636. - - This is a 'high-entropy cryptographic random string' that will be - impractical for an attacker to guess. - - Args: - n_bytes: integer between 31 and 96, inclusive. default: 64 - number of bytes of entropy to include in verifier. - - Returns: - Bytestring, representing urlsafe base64-encoded random data. - """ - verifier = base64.urlsafe_b64encode(os.urandom(n_bytes)).rstrip(b'=') - # https://tools.ietf.org/html/rfc7636#section-4.1 - # minimum length of 43 characters and a maximum length of 128 characters. - if len(verifier) < 43: - raise ValueError("Verifier too short. n_bytes must be > 30.") - elif len(verifier) > 128: - raise ValueError("Verifier too long. n_bytes must be < 97.") - else: - return verifier - - -def code_challenge(verifier): - """ - Creates a 'code_challenge' as described in section 4.2 of RFC 7636 - by taking the sha256 hash of the verifier and then urlsafe - base64-encoding it. - - Args: - verifier: bytestring, representing a code_verifier as generated by - code_verifier(). - - Returns: - Bytestring, representing a urlsafe base64-encoded sha256 hash digest, - without '=' padding. - """ - digest = hashlib.sha256(verifier).digest() - return base64.urlsafe_b64encode(digest).rstrip(b'=') diff --git a/oauth2client/client.py b/oauth2client/client.py index 7618960..8956443 100644 --- a/oauth2client/client.py +++ b/oauth2client/client.py @@ -34,11 +34,13 @@ from six.moves import urllib import oauth2client from oauth2client import _helpers -from oauth2client import _pkce from oauth2client import clientsecrets from oauth2client import transport +from oauth2client import util +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + HAS_OPENSSL = False HAS_CRYPTO = False try: @@ -98,20 +100,20 @@ AccessTokenInfo = collections.namedtuple( DEFAULT_ENV_NAME = 'UNKNOWN' # If set to True _get_environment avoid GCE check (_detect_gce_environment) -NO_GCE_CHECK = os.getenv('NO_GCE_CHECK', 'False') +NO_GCE_CHECK = os.environ.setdefault('NO_GCE_CHECK', 'False') # Timeout in seconds to wait for the GCE metadata server when detecting the # GCE environment. try: - GCE_METADATA_TIMEOUT = int(os.getenv('GCE_METADATA_TIMEOUT', 3)) + GCE_METADATA_TIMEOUT = int( + os.environ.setdefault('GCE_METADATA_TIMEOUT', '3')) except ValueError: # pragma: NO COVER GCE_METADATA_TIMEOUT = 3 _SERVER_SOFTWARE = 'SERVER_SOFTWARE' -_GCE_METADATA_URI = 'http://' + os.getenv('GCE_METADATA_IP', '169.254.169.254') -_METADATA_FLAVOR_HEADER = 'metadata-flavor' # lowercase header +_GCE_METADATA_HOST = '169.254.169.254' +_METADATA_FLAVOR_HEADER = 'Metadata-Flavor' _DESIRED_METADATA_FLAVOR = 'Google' -_GCE_HEADERS = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR} # Expose utcnow() at module level to allow for # easier testing (by replacing with a stub). @@ -438,6 +440,23 @@ class Storage(object): self.release_lock() +def _update_query_params(uri, params): + """Updates a URI with new query parameters. + + Args: + uri: string, A valid URI, with potential existing query parameters. + params: dict, A dictionary of query parameters. + + Returns: + The same URI but with the new query parameters added. + """ + parts = urllib.parse.urlparse(uri) + query_params = dict(urllib.parse.parse_qsl(parts.query)) + query_params.update(params) + new_parts = parts._replace(query=urllib.parse.urlencode(query_params)) + return urllib.parse.urlunparse(new_parts) + + class OAuth2Credentials(Credentials): """Credentials object for OAuth 2.0. @@ -447,11 +466,11 @@ class OAuth2Credentials(Credentials): OAuth2Credentials objects may be safely pickled and unpickled. """ - @_helpers.positional(8) + @util.positional(8) def __init__(self, access_token, client_id, client_secret, refresh_token, token_expiry, token_uri, user_agent, revoke_uri=None, id_token=None, token_response=None, scopes=None, - token_info_uri=None, id_token_jwt=None): + token_info_uri=None): """Create an instance of OAuth2Credentials. This constructor is not usually called by the user, instead @@ -474,11 +493,8 @@ class OAuth2Credentials(Credentials): because some providers (e.g. wordpress.com) include extra fields that clients may want. scopes: list, authorized scopes for these credentials. - token_info_uri: string, the URI for the token info endpoint. - Defaults to None; scopes can not be refreshed if - this is None. - id_token_jwt: string, the encoded and signed identity JWT. The - decoded version of this is stored in id_token. + token_info_uri: string, the URI for the token info endpoint. Defaults + to None; scopes can not be refreshed if this is None. Notes: store: callable, A callable that when passed a Credential @@ -496,9 +512,8 @@ class OAuth2Credentials(Credentials): self.user_agent = user_agent self.revoke_uri = revoke_uri self.id_token = id_token - self.id_token_jwt = id_token_jwt self.token_response = token_response - self.scopes = set(_helpers.string_to_scopes(scopes or [])) + self.scopes = set(util.string_to_scopes(scopes or [])) self.token_info_uri = token_info_uri # True if the credentials have been revoked or expired and can't be @@ -542,7 +557,7 @@ class OAuth2Credentials(Credentials): http: httplib2.Http, an http object to be used to make the refresh request. """ - self._refresh(http) + self._refresh(http.request) def revoke(self, http): """Revokes a refresh_token and makes the credentials void. @@ -551,7 +566,7 @@ class OAuth2Credentials(Credentials): http: httplib2.Http, an http object to be used to make the revoke request. """ - self._revoke(http) + self._revoke(http.request) def apply(self, headers): """Add the authorization to the headers. @@ -577,7 +592,7 @@ class OAuth2Credentials(Credentials): not have scopes. In both cases, you can use refresh_scopes() to obtain the canonical set of scopes. """ - scopes = _helpers.string_to_scopes(scopes) + scopes = util.string_to_scopes(scopes) return set(scopes).issubset(self.scopes) def retrieve_scopes(self, http): @@ -592,7 +607,7 @@ class OAuth2Credentials(Credentials): Returns: A set of strings containing the canonical list of scopes. """ - self._retrieve_scopes(http) + self._retrieve_scopes(http.request) return self.scopes @classmethod @@ -625,7 +640,6 @@ class OAuth2Credentials(Credentials): data['user_agent'], revoke_uri=data.get('revoke_uri', None), id_token=data.get('id_token', None), - id_token_jwt=data.get('id_token_jwt', None), token_response=data.get('token_response', None), scopes=data.get('scopes', None), token_info_uri=data.get('token_info_uri', None)) @@ -732,7 +746,7 @@ class OAuth2Credentials(Credentials): return headers - def _refresh(self, http): + def _refresh(self, http_request): """Refreshes the access_token. This method first checks by reading the Storage object if available. @@ -740,13 +754,15 @@ class OAuth2Credentials(Credentials): refresh is completed. Args: - http: an object to be used to make HTTP requests. + http_request: callable, a callable that matches the method + signature of httplib2.Http.request, used to make the + refresh request. Raises: HttpAccessTokenRefreshError: When the refresh fails. """ if not self.store: - self._do_refresh_request(http) + self._do_refresh_request(http_request) else: self.store.acquire_lock() try: @@ -758,15 +774,17 @@ class OAuth2Credentials(Credentials): logger.info('Updated access_token read from Storage') self._updateFromCredential(new_cred) else: - self._do_refresh_request(http) + self._do_refresh_request(http_request) finally: self.store.release_lock() - def _do_refresh_request(self, http): + def _do_refresh_request(self, http_request): """Refresh the access_token using the refresh_token. Args: - http: an object to be used to make HTTP requests. + http_request: callable, a callable that matches the method + signature of httplib2.Http.request, used to make the + refresh request. Raises: HttpAccessTokenRefreshError: When the refresh fails. @@ -775,9 +793,8 @@ class OAuth2Credentials(Credentials): headers = self._generate_refresh_request_headers() logger.info('Refreshing access_token') - resp, content = transport.request( - http, self.token_uri, method='POST', - body=body, headers=headers) + resp, content = http_request( + self.token_uri, method='POST', body=body, headers=headers) content = _helpers._from_bytes(content) if resp.status == http_client.OK: d = json.loads(content) @@ -791,10 +808,8 @@ class OAuth2Credentials(Credentials): self.token_expiry = None if 'id_token' in d: self.id_token = _extract_id_token(d['id_token']) - self.id_token_jwt = d['id_token'] else: self.id_token = None - self.id_token_jwt = None # On temporary refresh errors, the user does not actually have to # re-authorize, so we unflag here. self.invalid = False @@ -804,7 +819,7 @@ class OAuth2Credentials(Credentials): # An {'error':...} response body means the token is expired or # revoked, so we flag the credentials as such. logger.info('Failed to retrieve access token: %s', content) - error_msg = 'Invalid response {0}.'.format(resp.status) + error_msg = 'Invalid response {0}.'.format(resp['status']) try: d = json.loads(content) if 'error' in d: @@ -818,19 +833,23 @@ class OAuth2Credentials(Credentials): pass raise HttpAccessTokenRefreshError(error_msg, status=resp.status) - def _revoke(self, http): + def _revoke(self, http_request): """Revokes this credential and deletes the stored copy (if it exists). Args: - http: an object to be used to make HTTP requests. + http_request: callable, a callable that matches the method + signature of httplib2.Http.request, used to make the + revoke request. """ - self._do_revoke(http, self.refresh_token or self.access_token) + self._do_revoke(http_request, self.refresh_token or self.access_token) - def _do_revoke(self, http, token): + def _do_revoke(self, http_request, token): """Revokes this credential and deletes the stored copy (if it exists). Args: - http: an object to be used to make HTTP requests. + http_request: callable, a callable that matches the method + signature of httplib2.Http.request, used to make the + refresh request. token: A string used as the token to be revoked. Can be either an access_token or refresh_token. @@ -840,13 +859,8 @@ class OAuth2Credentials(Credentials): """ logger.info('Revoking token') query_params = {'token': token} - token_revoke_uri = _helpers.update_query_params( - self.revoke_uri, query_params) - resp, content = transport.request(http, token_revoke_uri) - if resp.status == http_client.METHOD_NOT_ALLOWED: - body = urllib.parse.urlencode(query_params) - resp, content = transport.request(http, token_revoke_uri, - method='POST', body=body) + token_revoke_uri = _update_query_params(self.revoke_uri, query_params) + resp, content = http_request(token_revoke_uri) if resp.status == http_client.OK: self.invalid = True else: @@ -862,19 +876,23 @@ class OAuth2Credentials(Credentials): if self.store: self.store.delete() - def _retrieve_scopes(self, http): + def _retrieve_scopes(self, http_request): """Retrieves the list of authorized scopes from the OAuth2 provider. Args: - http: an object to be used to make HTTP requests. + http_request: callable, a callable that matches the method + signature of httplib2.Http.request, used to make the + revoke request. """ - self._do_retrieve_scopes(http, self.access_token) + self._do_retrieve_scopes(http_request, self.access_token) - def _do_retrieve_scopes(self, http, token): + def _do_retrieve_scopes(self, http_request, token): """Retrieves the list of authorized scopes from the OAuth2 provider. Args: - http: an object to be used to make HTTP requests. + http_request: callable, a callable that matches the method + signature of httplib2.Http.request, used to make the + refresh request. token: A string used as the token to identify the credentials to the provider. @@ -884,13 +902,13 @@ class OAuth2Credentials(Credentials): """ logger.info('Refreshing scopes') query_params = {'access_token': token, 'fields': 'scope'} - token_info_uri = _helpers.update_query_params( - self.token_info_uri, query_params) - resp, content = transport.request(http, token_info_uri) + token_info_uri = _update_query_params(self.token_info_uri, + query_params) + resp, content = http_request(token_info_uri) content = _helpers._from_bytes(content) if resp.status == http_client.OK: d = json.loads(content) - self.scopes = set(_helpers.string_to_scopes(d.get('scope', ''))) + self.scopes = set(util.string_to_scopes(d.get('scope', ''))) else: error_msg = 'Invalid response {0}.'.format(resp.status) try: @@ -959,25 +977,19 @@ class AccessTokenCredentials(OAuth2Credentials): data['user_agent']) return retval - def _refresh(self, http): - """Refreshes the access token. - - Args: - http: unused HTTP object. - - Raises: - AccessTokenCredentialsError: always - """ + def _refresh(self, http_request): raise AccessTokenCredentialsError( 'The access_token is expired or invalid and can\'t be refreshed.') - def _revoke(self, http): + def _revoke(self, http_request): """Revokes the access_token and deletes the store if available. Args: - http: an object to be used to make HTTP requests. + http_request: callable, a callable that matches the method + signature of httplib2.Http.request, used to make the + revoke request. """ - self._do_revoke(http, self.access_token) + self._do_revoke(http_request, self.access_token) def _detect_gce_environment(): @@ -993,16 +1005,21 @@ def _detect_gce_environment(): # could lead to false negatives in the event that we are on GCE, but # the metadata resolution was particularly slow. The latter case is # "unlikely". - http = transport.get_http_object(timeout=GCE_METADATA_TIMEOUT) + connection = six.moves.http_client.HTTPConnection( + _GCE_METADATA_HOST, timeout=GCE_METADATA_TIMEOUT) + try: - response, _ = transport.request( - http, _GCE_METADATA_URI, headers=_GCE_HEADERS) - return ( - response.status == http_client.OK and - response.get(_METADATA_FLAVOR_HEADER) == _DESIRED_METADATA_FLAVOR) + headers = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR} + connection.request('GET', '/', headers=headers) + response = connection.getresponse() + if response.status == http_client.OK: + return (response.getheader(_METADATA_FLAVOR_HEADER) == + _DESIRED_METADATA_FLAVOR) except socket.error: # socket.timeout or socket.error(64, 'Host is down') logger.info('Timeout attempting to reach GCE metadata service.') return False + finally: + connection.close() def _in_gae_environment(): @@ -1452,7 +1469,7 @@ class AssertionCredentials(GoogleCredentials): AssertionCredentials objects may be safely pickled and unpickled. """ - @_helpers.positional(2) + @util.positional(2) def __init__(self, assertion_type, user_agent=None, token_uri=oauth2client.GOOGLE_TOKEN_URI, revoke_uri=oauth2client.GOOGLE_REVOKE_URI, @@ -1494,13 +1511,15 @@ class AssertionCredentials(GoogleCredentials): """Generate assertion string to be used in the access token request.""" raise NotImplementedError - def _revoke(self, http): + def _revoke(self, http_request): """Revokes the access_token and deletes the store if available. Args: - http: an object to be used to make HTTP requests. + http_request: callable, a callable that matches the method + signature of httplib2.Http.request, used to make the + revoke request. """ - self._do_revoke(http, self.access_token) + self._do_revoke(http_request, self.access_token) def sign_blob(self, blob): """Cryptographically sign a blob (of bytes). @@ -1526,7 +1545,7 @@ def _require_crypto_or_die(): raise CryptoUnavailableError('No crypto library available') -@_helpers.positional(2) +@util.positional(2) def verify_id_token(id_token, audience, http=None, cert_uri=ID_TOKEN_VERIFICATION_CERTS): """Verifies a signed JWT id_token. @@ -1553,7 +1572,7 @@ def verify_id_token(id_token, audience, http=None, if http is None: http = transport.get_cached_http() - resp, content = transport.request(http, cert_uri) + resp, content = http.request(cert_uri) if resp.status == http_client.OK: certs = json.loads(_helpers._from_bytes(content)) return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) @@ -1605,7 +1624,7 @@ def _parse_exchange_token_response(content): except Exception: # different JSON libs raise different exceptions, # so we just do a catch-all here - resp = _helpers.parse_unique_urlencoded(content) + resp = dict(urllib.parse.parse_qsl(content)) # some providers respond with 'expires', others with 'expires_in' if resp and 'expires' in resp: @@ -1614,7 +1633,7 @@ def _parse_exchange_token_response(content): return resp -@_helpers.positional(4) +@util.positional(4) def credentials_from_code(client_id, client_secret, scope, code, redirect_uri='postmessage', http=None, user_agent=None, @@ -1622,9 +1641,7 @@ def credentials_from_code(client_id, client_secret, scope, code, auth_uri=oauth2client.GOOGLE_AUTH_URI, revoke_uri=oauth2client.GOOGLE_REVOKE_URI, device_uri=oauth2client.GOOGLE_DEVICE_URI, - token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI, - pkce=False, - code_verifier=None): + token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI): """Exchanges an authorization code for an OAuth2Credentials object. Args: @@ -1648,15 +1665,6 @@ def credentials_from_code(client_id, client_secret, scope, code, device_uri: string, URI for device authorization endpoint. For convenience defaults to Google's endpoints but any OAuth 2.0 provider can be used. - pkce: boolean, default: False, Generate and include a "Proof Key - for Code Exchange" (PKCE) with your authorization and token - requests. This adds security for installed applications that - cannot protect a client_secret. See RFC 7636 for details. - code_verifier: bytestring or None, default: None, parameter passed - as part of the code exchange when pkce=True. If - None, a code_verifier will automatically be - generated as part of step1_get_authorize_url(). See - RFC 7636 for details. Returns: An OAuth2Credentials object. @@ -1667,20 +1675,16 @@ def credentials_from_code(client_id, client_secret, scope, code, """ flow = OAuth2WebServerFlow(client_id, client_secret, scope, redirect_uri=redirect_uri, - user_agent=user_agent, - auth_uri=auth_uri, - token_uri=token_uri, - revoke_uri=revoke_uri, + user_agent=user_agent, auth_uri=auth_uri, + token_uri=token_uri, revoke_uri=revoke_uri, device_uri=device_uri, - token_info_uri=token_info_uri, - pkce=pkce, - code_verifier=code_verifier) + token_info_uri=token_info_uri) credentials = flow.step2_exchange(code, http=http) return credentials -@_helpers.positional(3) +@util.positional(3) def credentials_from_clientsecrets_and_code(filename, scope, code, message=None, redirect_uri='postmessage', @@ -1709,15 +1713,6 @@ def credentials_from_clientsecrets_and_code(filename, scope, code, cache: An optional cache service client that implements get() and set() methods. See clientsecrets.loadfile() for details. device_uri: string, OAuth 2.0 device authorization endpoint - pkce: boolean, default: False, Generate and include a "Proof Key - for Code Exchange" (PKCE) with your authorization and token - requests. This adds security for installed applications that - cannot protect a client_secret. See RFC 7636 for details. - code_verifier: bytestring or None, default: None, parameter passed - as part of the code exchange when pkce=True. If - None, a code_verifier will automatically be - generated as part of step1_get_authorize_url(). See - RFC 7636 for details. Returns: An OAuth2Credentials object. @@ -1808,7 +1803,7 @@ class OAuth2WebServerFlow(Flow): OAuth2WebServerFlow objects may be safely pickled and unpickled. """ - @_helpers.positional(4) + @util.positional(4) def __init__(self, client_id, client_secret=None, scope=None, @@ -1821,8 +1816,6 @@ class OAuth2WebServerFlow(Flow): device_uri=oauth2client.GOOGLE_DEVICE_URI, token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI, authorization_header=None, - pkce=False, - code_verifier=None, **kwargs): """Constructor for OAuth2WebServerFlow. @@ -1860,15 +1853,6 @@ class OAuth2WebServerFlow(Flow): require a client to authenticate using a header value instead of passing client_secret in the POST body. - pkce: boolean, default: False, Generate and include a "Proof Key - for Code Exchange" (PKCE) with your authorization and token - requests. This adds security for installed applications that - cannot protect a client_secret. See RFC 7636 for details. - code_verifier: bytestring or None, default: None, parameter passed - as part of the code exchange when pkce=True. If - None, a code_verifier will automatically be - generated as part of step1_get_authorize_url(). See - RFC 7636 for details. **kwargs: dict, The keyword arguments are all optional and required parameters for the OAuth calls. """ @@ -1878,7 +1862,7 @@ class OAuth2WebServerFlow(Flow): raise TypeError("The value of scope must not be None") self.client_id = client_id self.client_secret = client_secret - self.scope = _helpers.scopes_to_string(scope) + self.scope = util.scopes_to_string(scope) self.redirect_uri = redirect_uri self.login_hint = login_hint self.user_agent = user_agent @@ -1888,11 +1872,9 @@ class OAuth2WebServerFlow(Flow): self.device_uri = device_uri self.token_info_uri = token_info_uri self.authorization_header = authorization_header - self._pkce = pkce - self.code_verifier = code_verifier self.params = _oauth2_web_server_flow_params(kwargs) - @_helpers.positional(1) + @util.positional(1) def step1_get_authorize_url(self, redirect_uri=None, state=None): """Returns a URI to redirect to the provider. @@ -1930,17 +1912,10 @@ class OAuth2WebServerFlow(Flow): query_params['state'] = state if self.login_hint is not None: query_params['login_hint'] = self.login_hint - if self._pkce: - if not self.code_verifier: - self.code_verifier = _pkce.code_verifier() - challenge = _pkce.code_challenge(self.code_verifier) - query_params['code_challenge'] = challenge - query_params['code_challenge_method'] = 'S256' - query_params.update(self.params) - return _helpers.update_query_params(self.auth_uri, query_params) + return _update_query_params(self.auth_uri, query_params) - @_helpers.positional(1) + @util.positional(1) def step1_get_device_and_user_codes(self, http=None): """Returns a user code and the verification URL where to enter it @@ -1965,8 +1940,8 @@ class OAuth2WebServerFlow(Flow): if http is None: http = transport.get_http_object() - resp, content = transport.request( - http, self.device_uri, method='POST', body=body, headers=headers) + resp, content = http.request(self.device_uri, method='POST', body=body, + headers=headers) content = _helpers._from_bytes(content) if resp.status == http_client.OK: try: @@ -1988,7 +1963,7 @@ class OAuth2WebServerFlow(Flow): pass raise OAuth2DeviceCodeError(error_msg) - @_helpers.positional(2) + @util.positional(2) def step2_exchange(self, code=None, http=None, device_flow_info=None): """Exchanges a code for OAuth2Credentials. @@ -2031,8 +2006,6 @@ class OAuth2WebServerFlow(Flow): } if self.client_secret is not None: post_data['client_secret'] = self.client_secret - if self._pkce: - post_data['code_verifier'] = self.code_verifier if device_flow_info is not None: post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0' else: @@ -2050,8 +2023,8 @@ class OAuth2WebServerFlow(Flow): if http is None: http = transport.get_http_object() - resp, content = transport.request( - http, self.token_uri, method='POST', body=body, headers=headers) + resp, content = http.request(self.token_uri, method='POST', body=body, + headers=headers) d = _parse_exchange_token_response(content) if resp.status == http_client.OK and 'access_token' in d: access_token = d['access_token'] @@ -2066,17 +2039,15 @@ class OAuth2WebServerFlow(Flow): token_expiry = delta + _UTCNOW() extracted_id_token = None - id_token_jwt = None if 'id_token' in d: extracted_id_token = _extract_id_token(d['id_token']) - id_token_jwt = d['id_token'] logger.info('Successfully retrieved access token') return OAuth2Credentials( access_token, self.client_id, self.client_secret, refresh_token, token_expiry, self.token_uri, self.user_agent, revoke_uri=self.revoke_uri, id_token=extracted_id_token, - id_token_jwt=id_token_jwt, token_response=d, scopes=self.scope, + token_response=d, scopes=self.scope, token_info_uri=self.token_info_uri) else: logger.info('Failed to retrieve access token: %s', content) @@ -2089,11 +2060,10 @@ class OAuth2WebServerFlow(Flow): raise FlowExchangeError(error_msg) -@_helpers.positional(2) +@util.positional(2) def flow_from_clientsecrets(filename, scope, redirect_uri=None, message=None, cache=None, login_hint=None, - device_uri=None, pkce=None, code_verifier=None, - prompt=None): + device_uri=None): """Create a Flow from a clientsecrets file. Will create the right kind of Flow based on the contents of the @@ -2142,17 +2112,10 @@ def flow_from_clientsecrets(filename, scope, redirect_uri=None, 'login_hint': login_hint, } revoke_uri = client_info.get('revoke_uri') - optional = ( - 'revoke_uri', - 'device_uri', - 'pkce', - 'code_verifier', - 'prompt' - ) - for param in optional: - if locals()[param] is not None: - constructor_kwargs[param] = locals()[param] - + if revoke_uri is not None: + constructor_kwargs['revoke_uri'] = revoke_uri + if device_uri is not None: + constructor_kwargs['device_uri'] = device_uri return OAuth2WebServerFlow( client_info['client_id'], client_info['client_secret'], scope, **constructor_kwargs) diff --git a/oauth2client/clientsecrets.py b/oauth2client/clientsecrets.py index 1598142..4b43e66 100644 --- a/oauth2client/clientsecrets.py +++ b/oauth2client/clientsecrets.py @@ -22,6 +22,7 @@ import json import six +__author__ = 'jcgregorio@google.com (Joe Gregorio)' # Properties that make a client_secrets.json file valid. TYPE_WEB = 'web' diff --git a/oauth2client/contrib/_fcntl_opener.py b/oauth2client/contrib/_fcntl_opener.py new file mode 100644 index 0000000..ae6c85b --- /dev/null +++ b/oauth2client/contrib/_fcntl_opener.py @@ -0,0 +1,81 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import errno +import fcntl +import time + +from oauth2client.contrib import locked_file + + +class _FcntlOpener(locked_file._Opener): + """Open, lock, and unlock a file using fcntl.lockf.""" + + def open_and_lock(self, timeout, delay): + """Open the file and lock it. + + Args: + timeout: float, How long to try to lock for. + delay: float, How long to wait between retries + + Raises: + AlreadyLockedException: if the lock is already acquired. + IOError: if the open fails. + CredentialsFileSymbolicLinkError: if the file is a symbolic + link. + """ + if self._locked: + raise locked_file.AlreadyLockedException( + 'File {0} is already locked'.format(self._filename)) + start_time = time.time() + + locked_file.validate_file(self._filename) + try: + self._fh = open(self._filename, self._mode) + except IOError as e: + # If we can't access with _mode, try _fallback_mode and + # don't lock. + if e.errno in (errno.EPERM, errno.EACCES): + self._fh = open(self._filename, self._fallback_mode) + return + + # We opened in _mode, try to lock the file. + while True: + try: + fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX) + self._locked = True + return + except IOError as e: + # If not retrying, then just pass on the error. + if timeout == 0: + raise + if e.errno != errno.EACCES: + raise + # We could not acquire the lock. Try again. + if (time.time() - start_time) >= timeout: + locked_file.logger.warn('Could not lock %s in %s seconds', + self._filename, timeout) + if self._fh: + self._fh.close() + self._fh = open(self._filename, self._fallback_mode) + return + time.sleep(delay) + + def unlock_and_close(self): + """Close and unlock the file using the fcntl.lockf primitive.""" + if self._locked: + fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN) + self._locked = False + if self._fh: + self._fh.close() diff --git a/oauth2client/contrib/_metadata.py b/oauth2client/contrib/_metadata.py index 564cd39..10e6a69 100644 --- a/oauth2client/contrib/_metadata.py +++ b/oauth2client/contrib/_metadata.py @@ -19,28 +19,29 @@ See https://cloud.google.com/compute/docs/metadata import datetime import json -import os +import httplib2 from six.moves import http_client from six.moves.urllib import parse as urlparse from oauth2client import _helpers from oauth2client import client -from oauth2client import transport +from oauth2client import util -METADATA_ROOT = 'http://{}/computeMetadata/v1/'.format( - os.getenv('GCE_METADATA_ROOT', 'metadata.google.internal')) +METADATA_ROOT = 'http://metadata.google.internal/computeMetadata/v1/' METADATA_HEADERS = {'Metadata-Flavor': 'Google'} -def get(http, path, root=METADATA_ROOT, recursive=None): +def get(http_request, path, root=METADATA_ROOT, recursive=None): """Fetch a resource from the metadata server. Args: - http: an object to be used to make HTTP requests. path: A string indicating the resource to retrieve. For example, - 'instance/service-accounts/default' + 'instance/service-accounts/defualt' + http_request: A callable that matches the method + signature of httplib2.Http.request. Used to make the request to the + metadataserver. root: A string indicating the full path to the metadata server root. recursive: A boolean indicating whether to do a recursive query of metadata. See @@ -50,14 +51,15 @@ def get(http, path, root=METADATA_ROOT, recursive=None): A dictionary if the metadata server returns JSON, otherwise a string. Raises: - http_client.HTTPException if an error corrured while - retrieving metadata. + httplib2.Httplib2Error if an error corrured while retrieving metadata. """ url = urlparse.urljoin(root, path) - url = _helpers._add_query_parameter(url, 'recursive', recursive) + url = util._add_query_parameter(url, 'recursive', recursive) - response, content = transport.request( - http, url, headers=METADATA_HEADERS) + response, content = http_request( + url, + headers=METADATA_HEADERS + ) if response.status == http_client.OK: decoded = _helpers._from_bytes(content) @@ -66,20 +68,21 @@ def get(http, path, root=METADATA_ROOT, recursive=None): else: return decoded else: - raise http_client.HTTPException( + raise httplib2.HttpLib2Error( 'Failed to retrieve {0} from the Google Compute Engine' 'metadata service. Response:\n{1}'.format(url, response)) -def get_service_account_info(http, service_account='default'): +def get_service_account_info(http_request, service_account='default'): """Get information about a service account from the metadata server. Args: - http: an object to be used to make HTTP requests. service_account: An email specifying the service account for which to look up information. Default will be information for the "default" service account of the current compute engine instance. - + http_request: A callable that matches the method + signature of httplib2.Http.request. Used to make the request to the + metadata server. Returns: A dictionary with information about the specified service account, for example: @@ -91,19 +94,21 @@ def get_service_account_info(http, service_account='default'): } """ return get( - http, + http_request, 'instance/service-accounts/{0}/'.format(service_account), recursive=True) -def get_token(http, service_account='default'): +def get_token(http_request, service_account='default'): """Fetch an oauth token for the Args: - http: an object to be used to make HTTP requests. service_account: An email specifying the service account this token should represent. Default will be a token for the "default" service account of the current compute engine instance. + http_request: A callable that matches the method + signature of httplib2.Http.request. Used to make the request to the + metadataserver. Returns: A tuple of (access token, token expiration), where access token is the @@ -111,7 +116,7 @@ def get_token(http, service_account='default'): that indicates when the access token will expire. """ token_json = get( - http, + http_request, 'instance/service-accounts/{0}/token'.format(service_account)) token_expiry = client._UTCNOW() + datetime.timedelta( seconds=token_json['expires_in']) diff --git a/oauth2client/contrib/_win32_opener.py b/oauth2client/contrib/_win32_opener.py new file mode 100644 index 0000000..34b4f48 --- /dev/null +++ b/oauth2client/contrib/_win32_opener.py @@ -0,0 +1,106 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import errno +import time + +import pywintypes +import win32con +import win32file + +from oauth2client.contrib import locked_file + + +class _Win32Opener(locked_file._Opener): + """Open, lock, and unlock a file using windows primitives.""" + + # Error #33: + # 'The process cannot access the file because another process' + FILE_IN_USE_ERROR = 33 + + # Error #158: + # 'The segment is already unlocked.' + FILE_ALREADY_UNLOCKED_ERROR = 158 + + def open_and_lock(self, timeout, delay): + """Open the file and lock it. + + Args: + timeout: float, How long to try to lock for. + delay: float, How long to wait between retries + + Raises: + AlreadyLockedException: if the lock is already acquired. + IOError: if the open fails. + CredentialsFileSymbolicLinkError: if the file is a symbolic + link. + """ + if self._locked: + raise locked_file.AlreadyLockedException( + 'File {0} is already locked'.format(self._filename)) + start_time = time.time() + + locked_file.validate_file(self._filename) + try: + self._fh = open(self._filename, self._mode) + except IOError as e: + # If we can't access with _mode, try _fallback_mode + # and don't lock. + if e.errno == errno.EACCES: + self._fh = open(self._filename, self._fallback_mode) + return + + # We opened in _mode, try to lock the file. + while True: + try: + hfile = win32file._get_osfhandle(self._fh.fileno()) + win32file.LockFileEx( + hfile, + (win32con.LOCKFILE_FAIL_IMMEDIATELY | + win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000, + pywintypes.OVERLAPPED()) + self._locked = True + return + except pywintypes.error as e: + if timeout == 0: + raise + + # If the error is not that the file is already + # in use, raise. + if e[0] != _Win32Opener.FILE_IN_USE_ERROR: + raise + + # We could not acquire the lock. Try again. + if (time.time() - start_time) >= timeout: + locked_file.logger.warn('Could not lock %s in %s seconds', + self._filename, timeout) + if self._fh: + self._fh.close() + self._fh = open(self._filename, self._fallback_mode) + return + time.sleep(delay) + + def unlock_and_close(self): + """Close and unlock the file using the win32 primitive.""" + if self._locked: + try: + hfile = win32file._get_osfhandle(self._fh.fileno()) + win32file.UnlockFileEx(hfile, 0, -0x10000, + pywintypes.OVERLAPPED()) + except pywintypes.error as e: + if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR: + raise + self._locked = False + if self._fh: + self._fh.close() diff --git a/oauth2client/contrib/appengine.py b/oauth2client/contrib/appengine.py index c1326ee..661105e 100644 --- a/oauth2client/contrib/appengine.py +++ b/oauth2client/contrib/appengine.py @@ -29,13 +29,13 @@ from google.appengine.api import memcache from google.appengine.api import users from google.appengine.ext import db from google.appengine.ext.webapp.util import login_required +import httplib2 import webapp2 as webapp import oauth2client -from oauth2client import _helpers from oauth2client import client from oauth2client import clientsecrets -from oauth2client import transport +from oauth2client import util from oauth2client.contrib import xsrfutil # This is a temporary fix for a Google internal issue. @@ -45,6 +45,8 @@ except ImportError: # pragma: NO COVER _appengine_ndb = None +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + logger = logging.getLogger(__name__) OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns' @@ -129,7 +131,7 @@ class AppAssertionCredentials(client.AssertionCredentials): information to generate and refresh its own access tokens. """ - @_helpers.positional(2) + @util.positional(2) def __init__(self, scope, **kwargs): """Constructor for AppAssertionCredentials @@ -141,7 +143,7 @@ class AppAssertionCredentials(client.AssertionCredentials): or unspecified, the default service account for the app is used. """ - self.scope = _helpers.scopes_to_string(scope) + self.scope = util.scopes_to_string(scope) self._kwargs = kwargs self.service_account_id = kwargs.get('service_account_id', None) self._service_account_email = None @@ -155,15 +157,17 @@ class AppAssertionCredentials(client.AssertionCredentials): data = json.loads(json_data) return AppAssertionCredentials(data['scope']) - def _refresh(self, http): - """Refreshes the access token. + def _refresh(self, http_request): + """Refreshes the access_token. Since the underlying App Engine app_identity implementation does its own caching we can skip all the storage hoops and just to a refresh using the API. Args: - http: unused HTTP object + http_request: callable, a callable that matches the method + signature of httplib2.Http.request, used to make the + refresh request. Raises: AccessTokenRefreshError: When the refresh fails. @@ -301,7 +305,7 @@ class StorageByKeyName(client.Storage): and that entities are stored by key_name. """ - @_helpers.positional(4) + @util.positional(4) def __init__(self, model, key_name, property_name, cache=None, user=None): """Constructor for Storage. @@ -519,7 +523,7 @@ class OAuth2Decorator(object): flow = property(get_flow, set_flow) - @_helpers.positional(4) + @util.positional(4) def __init__(self, client_id, client_secret, scope, auth_uri=oauth2client.GOOGLE_AUTH_URI, token_uri=oauth2client.GOOGLE_TOKEN_URI, @@ -586,7 +590,7 @@ class OAuth2Decorator(object): self.credentials = None self._client_id = client_id self._client_secret = client_secret - self._scope = _helpers.scopes_to_string(scope) + self._scope = util.scopes_to_string(scope) self._auth_uri = auth_uri self._token_uri = token_uri self._revoke_uri = revoke_uri @@ -738,8 +742,7 @@ class OAuth2Decorator(object): *args: Positional arguments passed to httplib2.Http constructor. **kwargs: Positional arguments passed to httplib2.Http constructor. """ - return self.credentials.authorize( - transport.get_http_object(*args, **kwargs)) + return self.credentials.authorize(httplib2.Http(*args, **kwargs)) @property def callback_path(self): @@ -801,7 +804,7 @@ class OAuth2Decorator(object): if (decorator._token_response_param and credentials.token_response): resp_json = json.dumps(credentials.token_response) - redirect_uri = _helpers._add_query_parameter( + redirect_uri = util._add_query_parameter( redirect_uri, decorator._token_response_param, resp_json) @@ -845,7 +848,7 @@ class OAuth2DecoratorFromClientSecrets(OAuth2Decorator): """ - @_helpers.positional(3) + @util.positional(3) def __init__(self, filename, scope, message=None, cache=None, **kwargs): """Constructor @@ -888,7 +891,7 @@ class OAuth2DecoratorFromClientSecrets(OAuth2Decorator): self._message = 'Please configure your application for OAuth 2.0.' -@_helpers.positional(2) +@util.positional(2) def oauth2decorator_from_clientsecrets(filename, scope, message=None, cache=None): """Creates an OAuth2Decorator populated from a clientsecrets file. diff --git a/oauth2client/contrib/devshell.py b/oauth2client/contrib/devshell.py index 691765f..b8bb978 100644 --- a/oauth2client/contrib/devshell.py +++ b/oauth2client/contrib/devshell.py @@ -37,7 +37,6 @@ class CommunicationError(Error): class NoDevshellServer(Error): """Error when no Developer Shell server can be contacted.""" - # The request for credential information to the Developer Shell client socket # is always an empty PBLite-formatted JSON object, so just define it as a # constant. @@ -118,12 +117,7 @@ class DevshellCredentials(client.GoogleCredentials): user_agent) self._refresh(None) - def _refresh(self, http): - """Refreshes the access token. - - Args: - http: unused HTTP object - """ + def _refresh(self, http_request): self.devshell_response = _SendRecv() self.access_token = self.devshell_response.access_token expires_in = self.devshell_response.expires_in diff --git a/oauth2client/contrib/django_util/__init__.py b/oauth2client/contrib/django_util/__init__.py index 644a8f9..5449e32 100644 --- a/oauth2client/contrib/django_util/__init__.py +++ b/oauth2client/contrib/django_util/__init__.py @@ -52,9 +52,6 @@ Add the helper to your INSTALLED_APPS: This helper also requires the Django Session Middleware, so ``django.contrib.sessions.middleware`` should be in INSTALLED_APPS as well. -MIDDLEWARE or MIDDLEWARE_CLASSES (in Django versions <1.10) should also -contain the string 'django.contrib.sessions.middleware.SessionMiddleware'. - Add the client secrets created earlier to the settings. You can either specify the path to the credentials file in JSON format @@ -231,10 +228,10 @@ import importlib import django.conf from django.core import exceptions from django.core import urlresolvers +import httplib2 from six.moves.urllib import parse from oauth2client import clientsecrets -from oauth2client import transport from oauth2client.contrib import dictionary_storage from oauth2client.contrib.django_util import storage @@ -338,26 +335,16 @@ class OAuth2Settings(object): self.request_prefix = getattr(settings_instance, 'GOOGLE_OAUTH2_REQUEST_ATTRIBUTE', GOOGLE_OAUTH2_REQUEST_ATTRIBUTE) - info = _get_oauth2_client_id_and_secret(settings_instance) - self.client_id, self.client_secret = info - - # Django 1.10 deprecated MIDDLEWARE_CLASSES in favor of MIDDLEWARE - middleware_settings = getattr(settings_instance, 'MIDDLEWARE', None) - if middleware_settings is None: - middleware_settings = getattr( - settings_instance, 'MIDDLEWARE_CLASSES', None) - if middleware_settings is None: - raise exceptions.ImproperlyConfigured( - 'Django settings has neither MIDDLEWARE nor MIDDLEWARE_CLASSES' - 'configured') + self.client_id, self.client_secret = \ + _get_oauth2_client_id_and_secret(settings_instance) - if ('django.contrib.sessions.middleware.SessionMiddleware' not in - middleware_settings): + if ('django.contrib.sessions.middleware.SessionMiddleware' + not in settings_instance.MIDDLEWARE_CLASSES): raise exceptions.ImproperlyConfigured( - 'The Google OAuth2 Helper requires session middleware to ' - 'be installed. Edit your MIDDLEWARE_CLASSES or MIDDLEWARE ' - 'setting to include \'django.contrib.sessions.middleware.' - 'SessionMiddleware\'.') + 'The Google OAuth2 Helper requires session middleware to ' + 'be installed. Edit your MIDDLEWARE_CLASSES setting' + ' to include \'django.contrib.sessions.middleware.' + 'SessionMiddleware\'.') (self.storage_model, self.storage_model_user_property, self.storage_model_credentials_property) = _get_storage_model() @@ -483,7 +470,8 @@ class UserOAuth2(object): @property def http(self): - """Helper: create HTTP client authorized with OAuth2 credentials.""" + """Helper method to create an HTTP client authorized with OAuth2 + credentials.""" if self.has_credentials(): - return self.credentials.authorize(transport.get_http_object()) + return self.credentials.authorize(httplib2.Http()) return None diff --git a/oauth2client/contrib/django_util/models.py b/oauth2client/contrib/django_util/models.py index 37cc697..87e1da7 100644 --- a/oauth2client/contrib/django_util/models.py +++ b/oauth2client/contrib/django_util/models.py @@ -19,7 +19,6 @@ import pickle from django.db import models from django.utils import encoding -import jsonpickle import oauth2client @@ -49,12 +48,7 @@ class CredentialsField(models.Field): elif isinstance(value, oauth2client.client.Credentials): return value else: - try: - return jsonpickle.decode( - base64.b64decode(encoding.smart_bytes(value)).decode()) - except ValueError: - return pickle.loads( - base64.b64decode(encoding.smart_bytes(value))) + return pickle.loads(base64.b64decode(encoding.smart_bytes(value))) def get_prep_value(self, value): """Overrides ``models.Field`` method. This is used to convert @@ -64,8 +58,7 @@ class CredentialsField(models.Field): if value is None: return None else: - return encoding.smart_text( - base64.b64encode(jsonpickle.encode(value).encode())) + return encoding.smart_text(base64.b64encode(pickle.dumps(value))) def value_to_string(self, obj): """Convert the field value from the provided model to a string. diff --git a/oauth2client/contrib/django_util/views.py b/oauth2client/contrib/django_util/views.py index 1835208..4d8ae03 100644 --- a/oauth2client/contrib/django_util/views.py +++ b/oauth2client/contrib/django_util/views.py @@ -22,14 +22,13 @@ in the configured storage.""" import hashlib import json import os +import pickle from django import http from django import shortcuts from django.conf import settings from django.core import urlresolvers from django.shortcuts import redirect -from django.utils import html -import jsonpickle from six.moves.urllib import parse from oauth2client import client @@ -72,7 +71,7 @@ def _make_flow(request, scopes, return_url=None): urlresolvers.reverse("google_oauth:callback"))) flow_key = _FLOW_KEY.format(csrf_token) - request.session[flow_key] = jsonpickle.encode(flow) + request.session[flow_key] = pickle.dumps(flow) return flow @@ -90,7 +89,7 @@ def _get_flow_for_token(csrf_token, request): CSRF token. """ flow_pickle = request.session.get(_FLOW_KEY.format(csrf_token), None) - return None if flow_pickle is None else jsonpickle.decode(flow_pickle) + return None if flow_pickle is None else pickle.loads(flow_pickle) def oauth2_callback(request): @@ -110,7 +109,6 @@ def oauth2_callback(request): if 'error' in request.GET: reason = request.GET.get( 'error_description', request.GET.get('error', '')) - reason = html.escape(reason) return http.HttpResponseBadRequest( 'Authorization failed {0}'.format(reason)) @@ -172,10 +170,7 @@ def oauth2_authorize(request): A redirect to Google OAuth2 Authorization. """ return_url = request.GET.get('return_url', None) - if not return_url: - return_url = request.META.get('HTTP_REFERER', '/') - scopes = request.GET.getlist('scopes', django_util.oauth2_settings.scopes) # Model storage (but not session storage) requires a logged in user if django_util.oauth2_settings.storage_model: if not request.user.is_authenticated(): @@ -183,11 +178,13 @@ def oauth2_authorize(request): settings.LOGIN_URL, parse.quote(request.get_full_path()))) # This checks for the case where we ended up here because of a logged # out user but we had credentials for it in the first place - else: - user_oauth = django_util.UserOAuth2(request, scopes, return_url) - if user_oauth.has_credentials(): - return redirect(return_url) + elif get_storage(request).get() is not None: + return redirect(return_url) + scopes = request.GET.getlist('scopes', django_util.oauth2_settings.scopes) + + if not return_url: + return_url = request.META.get('HTTP_REFERER', '/') flow = _make_flow(request=request, scopes=scopes, return_url=return_url) auth_url = flow.step1_get_authorize_url() return shortcuts.redirect(auth_url) diff --git a/oauth2client/contrib/flask_util.py b/oauth2client/contrib/flask_util.py index fabd613..47c3df1 100644 --- a/oauth2client/contrib/flask_util.py +++ b/oauth2client/contrib/flask_util.py @@ -176,18 +176,19 @@ try: from flask import request from flask import session from flask import url_for - import markupsafe except ImportError: # pragma: NO COVER raise ImportError('The flask utilities require flask 0.9 or newer.') +import httplib2 import six.moves.http_client as httplib from oauth2client import client from oauth2client import clientsecrets -from oauth2client import transport from oauth2client.contrib import dictionary_storage +__author__ = 'jonwayne@google.com (Jon Wayne Parrott)' + _DEFAULT_SCOPES = ('email',) _CREDENTIALS_KEY = 'google_oauth2_credentials' _FLOW_KEY = 'google_oauth2_flow_{0}' @@ -389,7 +390,6 @@ class UserOAuth2(object): if 'error' in request.args: reason = request.args.get( 'error_description', request.args.get('error', '')) - reason = markupsafe.escape(reason) return ('Authorization failed: {0}'.format(reason), httplib.BAD_REQUEST) @@ -553,5 +553,4 @@ class UserOAuth2(object): """ if not self.credentials: raise ValueError('No credentials available.') - return self.credentials.authorize( - transport.get_http_object(*args, **kwargs)) + return self.credentials.authorize(httplib2.Http(*args, **kwargs)) diff --git a/oauth2client/contrib/gce.py b/oauth2client/contrib/gce.py index aaab15f..f3a6ca1 100644 --- a/oauth2client/contrib/gce.py +++ b/oauth2client/contrib/gce.py @@ -20,12 +20,14 @@ Utilities for making it easier to use OAuth 2.0 on Google Compute Engine. import logging import warnings -from six.moves import http_client +import httplib2 from oauth2client import client from oauth2client.contrib import _metadata +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + logger = logging.getLogger(__name__) _SCOPES_WARNING = """\ @@ -96,40 +98,44 @@ class AppAssertionCredentials(client.AssertionCredentials): Returns: A set of strings containing the canonical list of scopes. """ - self._retrieve_info(http) + self._retrieve_info(http.request) return self.scopes - def _retrieve_info(self, http): - """Retrieves service account info for invalid credentials. + def _retrieve_info(self, http_request): + """Validates invalid service accounts by retrieving service account info. Args: - http: an object to be used to make HTTP requests. + http_request: callable, a callable that matches the method + signature of httplib2.Http.request, used to make the + request to the metadata server """ if self.invalid: info = _metadata.get_service_account_info( - http, + http_request, service_account=self.service_account_email or 'default') self.invalid = False self.service_account_email = info['email'] self.scopes = info['scopes'] - def _refresh(self, http): - """Refreshes the access token. + def _refresh(self, http_request): + """Refreshes the access_token. Skip all the storage hoops and just refresh using the API. Args: - http: an object to be used to make HTTP requests. + http_request: callable, a callable that matches the method + signature of httplib2.Http.request, used to make + the refresh request. Raises: HttpAccessTokenRefreshError: When the refresh fails. """ try: - self._retrieve_info(http) + self._retrieve_info(http_request) self.access_token, self.token_expiry = _metadata.get_token( - http, service_account=self.service_account_email) - except http_client.HTTPException as err: - raise client.HttpAccessTokenRefreshError(str(err)) + http_request, service_account=self.service_account_email) + except httplib2.HttpLib2Error as e: + raise client.HttpAccessTokenRefreshError(str(e)) @property def serialization_data(self): diff --git a/oauth2client/contrib/keyring_storage.py b/oauth2client/contrib/keyring_storage.py index 4af9448..f4f2e30 100644 --- a/oauth2client/contrib/keyring_storage.py +++ b/oauth2client/contrib/keyring_storage.py @@ -24,6 +24,9 @@ import keyring from oauth2client import client +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + + class Storage(client.Storage): """Store and retrieve a single credential to and from the keyring. diff --git a/oauth2client/contrib/locked_file.py b/oauth2client/contrib/locked_file.py new file mode 100644 index 0000000..0d28ebb --- /dev/null +++ b/oauth2client/contrib/locked_file.py @@ -0,0 +1,234 @@ +# Copyright 2014 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Locked file interface that should work on Unix and Windows pythons. + +This module first tries to use fcntl locking to ensure serialized access +to a file, then falls back on a lock file if that is unavialable. + +Usage:: + + f = LockedFile('filename', 'r+b', 'rb') + f.open_and_lock() + if f.is_locked(): + print('Acquired filename with r+b mode') + f.file_handle().write('locked data') + else: + print('Acquired filename with rb mode') + f.unlock_and_close() + +""" + +from __future__ import print_function + +import errno +import logging +import os +import time + +from oauth2client import util + + +__author__ = 'cache@google.com (David T McWherter)' + +logger = logging.getLogger(__name__) + + +class CredentialsFileSymbolicLinkError(Exception): + """Credentials files must not be symbolic links.""" + + +class AlreadyLockedException(Exception): + """Trying to lock a file that has already been locked by the LockedFile.""" + pass + + +def validate_file(filename): + if os.path.islink(filename): + raise CredentialsFileSymbolicLinkError( + 'File: {0} is a symbolic link.'.format(filename)) + + +class _Opener(object): + """Base class for different locking primitives.""" + + def __init__(self, filename, mode, fallback_mode): + """Create an Opener. + + Args: + filename: string, The pathname of the file. + mode: string, The preferred mode to access the file with. + fallback_mode: string, The mode to use if locking fails. + """ + self._locked = False + self._filename = filename + self._mode = mode + self._fallback_mode = fallback_mode + self._fh = None + self._lock_fd = None + + def is_locked(self): + """Was the file locked.""" + return self._locked + + def file_handle(self): + """The file handle to the file. Valid only after opened.""" + return self._fh + + def filename(self): + """The filename that is being locked.""" + return self._filename + + def open_and_lock(self, timeout, delay): + """Open the file and lock it. + + Args: + timeout: float, How long to try to lock for. + delay: float, How long to wait between retries. + """ + pass + + def unlock_and_close(self): + """Unlock and close the file.""" + pass + + +class _PosixOpener(_Opener): + """Lock files using Posix advisory lock files.""" + + def open_and_lock(self, timeout, delay): + """Open the file and lock it. + + Tries to create a .lock file next to the file we're trying to open. + + Args: + timeout: float, How long to try to lock for. + delay: float, How long to wait between retries. + + Raises: + AlreadyLockedException: if the lock is already acquired. + IOError: if the open fails. + CredentialsFileSymbolicLinkError if the file is a symbolic link. + """ + if self._locked: + raise AlreadyLockedException( + 'File {0} is already locked'.format(self._filename)) + self._locked = False + + validate_file(self._filename) + try: + self._fh = open(self._filename, self._mode) + except IOError as e: + # If we can't access with _mode, try _fallback_mode and don't lock. + if e.errno == errno.EACCES: + self._fh = open(self._filename, self._fallback_mode) + return + + lock_filename = self._posix_lockfile(self._filename) + start_time = time.time() + while True: + try: + self._lock_fd = os.open(lock_filename, + os.O_CREAT | os.O_EXCL | os.O_RDWR) + self._locked = True + break + + except OSError as e: + if e.errno != errno.EEXIST: + raise + if (time.time() - start_time) >= timeout: + logger.warn('Could not acquire lock %s in %s seconds', + lock_filename, timeout) + # Close the file and open in fallback_mode. + if self._fh: + self._fh.close() + self._fh = open(self._filename, self._fallback_mode) + return + time.sleep(delay) + + def unlock_and_close(self): + """Unlock a file by removing the .lock file, and close the handle.""" + if self._locked: + lock_filename = self._posix_lockfile(self._filename) + os.close(self._lock_fd) + os.unlink(lock_filename) + self._locked = False + self._lock_fd = None + if self._fh: + self._fh.close() + + def _posix_lockfile(self, filename): + """The name of the lock file to use for posix locking.""" + return '{0}.lock'.format(filename) + + +class LockedFile(object): + """Represent a file that has exclusive access.""" + + @util.positional(4) + def __init__(self, filename, mode, fallback_mode, use_native_locking=True): + """Construct a LockedFile. + + Args: + filename: string, The path of the file to open. + mode: string, The mode to try to open the file with. + fallback_mode: string, The mode to use if locking fails. + use_native_locking: bool, Whether or not fcntl/win32 locking is + used. + """ + opener = None + if not opener and use_native_locking: + try: + from oauth2client.contrib._win32_opener import _Win32Opener + opener = _Win32Opener(filename, mode, fallback_mode) + except ImportError: + try: + from oauth2client.contrib._fcntl_opener import _FcntlOpener + opener = _FcntlOpener(filename, mode, fallback_mode) + except ImportError: + pass + + if not opener: + opener = _PosixOpener(filename, mode, fallback_mode) + + self._opener = opener + + def filename(self): + """Return the filename we were constructed with.""" + return self._opener._filename + + def file_handle(self): + """Return the file_handle to the opened file.""" + return self._opener.file_handle() + + def is_locked(self): + """Return whether we successfully locked the file.""" + return self._opener.is_locked() + + def open_and_lock(self, timeout=0, delay=0.05): + """Open the file, trying to lock it. + + Args: + timeout: float, The number of seconds to try to acquire the lock. + delay: float, The number of seconds to wait between retry attempts. + + Raises: + AlreadyLockedException: if the lock is already acquired. + IOError: if the open fails. + """ + self._opener.open_and_lock(timeout, delay) + + def unlock_and_close(self): + """Unlock and close a file.""" + self._opener.unlock_and_close() diff --git a/oauth2client/contrib/multistore_file.py b/oauth2client/contrib/multistore_file.py new file mode 100644 index 0000000..10f4cb4 --- /dev/null +++ b/oauth2client/contrib/multistore_file.py @@ -0,0 +1,505 @@ +# Copyright 2014 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Multi-credential file store with lock support. + +This module implements a JSON credential store where multiple +credentials can be stored in one file. That file supports locking +both in a single process and across processes. + +The credential themselves are keyed off of: + +* client_id +* user_agent +* scope + +The format of the stored data is like so:: + + { + 'file_version': 1, + 'data': [ + { + 'key': { + 'clientId': '', + 'userAgent': '', + 'scope': '' + }, + 'credential': { + # JSON serialized Credentials. + } + } + ] + } + +""" + +import errno +import json +import logging +import os +import threading + +from oauth2client import client +from oauth2client import util +from oauth2client.contrib import locked_file + +__author__ = 'jbeda@google.com (Joe Beda)' + +logger = logging.getLogger(__name__) + +logger.warning( + 'The oauth2client.contrib.multistore_file module has been deprecated and ' + 'will be removed in the next release of oauth2client. Please migrate to ' + 'multiprocess_file_storage.') + +# A dict from 'filename'->_MultiStore instances +_multistores = {} +_multistores_lock = threading.Lock() + + +class Error(Exception): + """Base error for this module.""" + + +class NewerCredentialStoreError(Error): + """The credential store is a newer version than supported.""" + + +def _dict_to_tuple_key(dictionary): + """Converts a dictionary to a tuple that can be used as an immutable key. + + The resulting key is always sorted so that logically equivalent + dictionaries always produce an identical tuple for a key. + + Args: + dictionary: the dictionary to use as the key. + + Returns: + A tuple representing the dictionary in it's naturally sorted ordering. + """ + return tuple(sorted(dictionary.items())) + + +@util.positional(4) +def get_credential_storage(filename, client_id, user_agent, scope, + warn_on_readonly=True): + """Get a Storage instance for a credential. + + Args: + filename: The JSON file storing a set of credentials + client_id: The client_id for the credential + user_agent: The user agent for the credential + scope: string or iterable of strings, Scope(s) being requested + warn_on_readonly: if True, log a warning if the store is readonly + + Returns: + An object derived from client.Storage for getting/setting the + credential. + """ + # Recreate the legacy key with these specific parameters + key = {'clientId': client_id, 'userAgent': user_agent, + 'scope': util.scopes_to_string(scope)} + return get_credential_storage_custom_key( + filename, key, warn_on_readonly=warn_on_readonly) + + +@util.positional(2) +def get_credential_storage_custom_string_key(filename, key_string, + warn_on_readonly=True): + """Get a Storage instance for a credential using a single string as a key. + + Allows you to provide a string as a custom key that will be used for + credential storage and retrieval. + + Args: + filename: The JSON file storing a set of credentials + key_string: A string to use as the key for storing this credential. + warn_on_readonly: if True, log a warning if the store is readonly + + Returns: + An object derived from client.Storage for getting/setting the + credential. + """ + # Create a key dictionary that can be used + key_dict = {'key': key_string} + return get_credential_storage_custom_key( + filename, key_dict, warn_on_readonly=warn_on_readonly) + + +@util.positional(2) +def get_credential_storage_custom_key(filename, key_dict, + warn_on_readonly=True): + """Get a Storage instance for a credential using a dictionary as a key. + + Allows you to provide a dictionary as a custom key that will be used for + credential storage and retrieval. + + Args: + filename: The JSON file storing a set of credentials + key_dict: A dictionary to use as the key for storing this credential. + There is no ordering of the keys in the dictionary. Logically + equivalent dictionaries will produce equivalent storage keys. + warn_on_readonly: if True, log a warning if the store is readonly + + Returns: + An object derived from client.Storage for getting/setting the + credential. + """ + multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly) + key = _dict_to_tuple_key(key_dict) + return multistore._get_storage(key) + + +@util.positional(1) +def get_all_credential_keys(filename, warn_on_readonly=True): + """Gets all the registered credential keys in the given Multistore. + + Args: + filename: The JSON file storing a set of credentials + warn_on_readonly: if True, log a warning if the store is readonly + + Returns: + A list of the credential keys present in the file. They are returned + as dictionaries that can be passed into + get_credential_storage_custom_key to get the actual credentials. + """ + multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly) + multistore._lock() + try: + return multistore._get_all_credential_keys() + finally: + multistore._unlock() + + +@util.positional(1) +def _get_multistore(filename, warn_on_readonly=True): + """A helper method to initialize the multistore with proper locking. + + Args: + filename: The JSON file storing a set of credentials + warn_on_readonly: if True, log a warning if the store is readonly + + Returns: + A multistore object + """ + filename = os.path.expanduser(filename) + _multistores_lock.acquire() + try: + multistore = _multistores.setdefault( + filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly)) + finally: + _multistores_lock.release() + return multistore + + +class _MultiStore(object): + """A file backed store for multiple credentials.""" + + @util.positional(2) + def __init__(self, filename, warn_on_readonly=True): + """Initialize the class. + + This will create the file if necessary. + """ + self._file = locked_file.LockedFile(filename, 'r+', 'r') + self._thread_lock = threading.Lock() + self._read_only = False + self._warn_on_readonly = warn_on_readonly + + self._create_file_if_needed() + + # Cache of deserialized store. This is only valid after the + # _MultiStore is locked or _refresh_data_cache is called. This is + # of the form of: + # + # ((key, value), (key, value)...) -> OAuth2Credential + # + # If this is None, then the store hasn't been read yet. + self._data = None + + class _Storage(client.Storage): + """A Storage object that can read/write a single credential.""" + + def __init__(self, multistore, key): + self._multistore = multistore + self._key = key + + def acquire_lock(self): + """Acquires any lock necessary to access this Storage. + + This lock is not reentrant. + """ + self._multistore._lock() + + def release_lock(self): + """Release the Storage lock. + + Trying to release a lock that isn't held will result in a + RuntimeError. + """ + self._multistore._unlock() + + def locked_get(self): + """Retrieve credential. + + The Storage lock must be held when this is called. + + Returns: + oauth2client.client.Credentials + """ + credential = self._multistore._get_credential(self._key) + if credential: + credential.set_store(self) + return credential + + def locked_put(self, credentials): + """Write a credential. + + The Storage lock must be held when this is called. + + Args: + credentials: Credentials, the credentials to store. + """ + self._multistore._update_credential(self._key, credentials) + + def locked_delete(self): + """Delete a credential. + + The Storage lock must be held when this is called. + + Args: + credentials: Credentials, the credentials to store. + """ + self._multistore._delete_credential(self._key) + + def _create_file_if_needed(self): + """Create an empty file if necessary. + + This method will not initialize the file. Instead it implements a + simple version of "touch" to ensure the file has been created. + """ + if not os.path.exists(self._file.filename()): + old_umask = os.umask(0o177) + try: + open(self._file.filename(), 'a+b').close() + finally: + os.umask(old_umask) + + def _lock(self): + """Lock the entire multistore.""" + self._thread_lock.acquire() + try: + self._file.open_and_lock() + except (IOError, OSError) as e: + if e.errno == errno.ENOSYS: + logger.warn('File system does not support locking the ' + 'credentials file.') + elif e.errno == errno.ENOLCK: + logger.warn('File system is out of resources for writing the ' + 'credentials file (is your disk full?).') + elif e.errno == errno.EDEADLK: + logger.warn('Lock contention on multistore file, opening ' + 'in read-only mode.') + elif e.errno == errno.EACCES: + logger.warn('Cannot access credentials file.') + else: + raise + if not self._file.is_locked(): + self._read_only = True + if self._warn_on_readonly: + logger.warn('The credentials file (%s) is not writable. ' + 'Opening in read-only mode. Any refreshed ' + 'credentials will only be ' + 'valid for this run.', self._file.filename()) + + if os.path.getsize(self._file.filename()) == 0: + logger.debug('Initializing empty multistore file') + # The multistore is empty so write out an empty file. + self._data = {} + self._write() + elif not self._read_only or self._data is None: + # Only refresh the data if we are read/write or we haven't + # cached the data yet. If we are readonly, we assume is isn't + # changing out from under us and that we only have to read it + # once. This prevents us from whacking any new access keys that + # we have cached in memory but were unable to write out. + self._refresh_data_cache() + + def _unlock(self): + """Release the lock on the multistore.""" + self._file.unlock_and_close() + self._thread_lock.release() + + def _locked_json_read(self): + """Get the raw content of the multistore file. + + The multistore must be locked when this is called. + + Returns: + The contents of the multistore decoded as JSON. + """ + assert self._thread_lock.locked() + self._file.file_handle().seek(0) + return json.load(self._file.file_handle()) + + def _locked_json_write(self, data): + """Write a JSON serializable data structure to the multistore. + + The multistore must be locked when this is called. + + Args: + data: The data to be serialized and written. + """ + assert self._thread_lock.locked() + if self._read_only: + return + self._file.file_handle().seek(0) + json.dump(data, self._file.file_handle(), + sort_keys=True, indent=2, separators=(',', ': ')) + self._file.file_handle().truncate() + + def _refresh_data_cache(self): + """Refresh the contents of the multistore. + + The multistore must be locked when this is called. + + Raises: + NewerCredentialStoreError: Raised when a newer client has written + the store. + """ + self._data = {} + try: + raw_data = self._locked_json_read() + except Exception: + logger.warn('Credential data store could not be loaded. ' + 'Will ignore and overwrite.') + return + + version = 0 + try: + version = raw_data['file_version'] + except Exception: + logger.warn('Missing version for credential data store. It may be ' + 'corrupt or an old version. Overwriting.') + if version > 1: + raise NewerCredentialStoreError( + 'Credential file has file_version of {0}. ' + 'Only file_version of 1 is supported.'.format(version)) + + credentials = [] + try: + credentials = raw_data['data'] + except (TypeError, KeyError): + pass + + for cred_entry in credentials: + try: + key, credential = self._decode_credential_from_json(cred_entry) + self._data[key] = credential + except: + # If something goes wrong loading a credential, just ignore it + logger.info('Error decoding credential, skipping', + exc_info=True) + + def _decode_credential_from_json(self, cred_entry): + """Load a credential from our JSON serialization. + + Args: + cred_entry: A dict entry from the data member of our format + + Returns: + (key, cred) where the key is the key tuple and the cred is the + OAuth2Credential object. + """ + raw_key = cred_entry['key'] + key = _dict_to_tuple_key(raw_key) + credential = None + credential = client.Credentials.new_from_json( + json.dumps(cred_entry['credential'])) + return (key, credential) + + def _write(self): + """Write the cached data back out. + + The multistore must be locked. + """ + raw_data = {'file_version': 1} + raw_creds = [] + raw_data['data'] = raw_creds + for (cred_key, cred) in self._data.items(): + raw_key = dict(cred_key) + raw_cred = json.loads(cred.to_json()) + raw_creds.append({'key': raw_key, 'credential': raw_cred}) + self._locked_json_write(raw_data) + + def _get_all_credential_keys(self): + """Gets all the registered credential keys in the multistore. + + Returns: + A list of dictionaries corresponding to all the keys currently + registered + """ + return [dict(key) for key in self._data.keys()] + + def _get_credential(self, key): + """Get a credential from the multistore. + + The multistore must be locked. + + Args: + key: The key used to retrieve the credential + + Returns: + The credential specified or None if not present + """ + return self._data.get(key, None) + + def _update_credential(self, key, cred): + """Update a credential and write the multistore. + + This must be called when the multistore is locked. + + Args: + key: The key used to retrieve the credential + cred: The OAuth2Credential to update/set + """ + self._data[key] = cred + self._write() + + def _delete_credential(self, key): + """Delete a credential and write the multistore. + + This must be called when the multistore is locked. + + Args: + key: The key used to retrieve the credential + """ + try: + del self._data[key] + except KeyError: + pass + self._write() + + def _get_storage(self, key): + """Get a Storage object to get/set a credential. + + This Storage is a 'view' into the multistore. + + Args: + key: The key used to retrieve the credential + + Returns: + A Storage object that can be used to get/set this cred + """ + return self._Storage(self, key) diff --git a/oauth2client/contrib/xsrfutil.py b/oauth2client/contrib/xsrfutil.py index 7c3ec03..c03e679 100644 --- a/oauth2client/contrib/xsrfutil.py +++ b/oauth2client/contrib/xsrfutil.py @@ -20,7 +20,12 @@ import hmac import time from oauth2client import _helpers +from oauth2client import util +__authors__ = [ + '"Doug Coker" ', + '"Joe Gregorio" ', +] # Delimiter character DELIMITER = b':' @@ -29,7 +34,7 @@ DELIMITER = b':' DEFAULT_TIMEOUT_SECS = 60 * 60 -@_helpers.positional(2) +@util.positional(2) def generate_token(key, user_id, action_id='', when=None): """Generates a URL-safe token for the given user, action, time tuple. @@ -57,7 +62,7 @@ def generate_token(key, user_id, action_id='', when=None): return token -@_helpers.positional(3) +@util.positional(3) def validate_token(key, token, user_id, action_id="", current_time=None): """Validates that the given token authorizes the user for the action. diff --git a/oauth2client/file.py b/oauth2client/file.py index 3551c80..feede11 100644 --- a/oauth2client/file.py +++ b/oauth2client/file.py @@ -21,10 +21,16 @@ credentials. import os import threading -from oauth2client import _helpers from oauth2client import client +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + + +class CredentialsFileSymbolicLinkError(Exception): + """Credentials files must not be symbolic links.""" + + class Storage(client.Storage): """Store and retrieve a single credential to and from a file.""" @@ -32,6 +38,11 @@ class Storage(client.Storage): super(Storage, self).__init__(lock=threading.Lock()) self._filename = filename + def _validate_file(self): + if os.path.islink(self._filename): + raise CredentialsFileSymbolicLinkError( + 'File: {0} is a symbolic link.'.format(self._filename)) + def locked_get(self): """Retrieve Credential from file. @@ -39,10 +50,10 @@ class Storage(client.Storage): oauth2client.client.Credentials Raises: - IOError if the file is a symbolic link. + CredentialsFileSymbolicLinkError if the file is a symbolic link. """ credentials = None - _helpers.validate_file(self._filename) + self._validate_file() try: f = open(self._filename, 'rb') content = f.read() @@ -78,10 +89,10 @@ class Storage(client.Storage): credentials: Credentials, the credentials to store. Raises: - IOError if the file is a symbolic link. + CredentialsFileSymbolicLinkError if the file is a symbolic link. """ self._create_file_if_needed() - _helpers.validate_file(self._filename) + self._validate_file() f = open(self._filename, 'w') f.write(credentials.to_json()) f.close() diff --git a/oauth2client/service_account.py b/oauth2client/service_account.py index 540bfaa..bdcfd69 100644 --- a/oauth2client/service_account.py +++ b/oauth2client/service_account.py @@ -25,6 +25,7 @@ from oauth2client import _helpers from oauth2client import client from oauth2client import crypt from oauth2client import transport +from oauth2client import util _PASSWORD_DEFAULT = 'notasecret' @@ -109,7 +110,7 @@ class ServiceAccountCredentials(client.AssertionCredentials): self._service_account_email = service_account_email self._signer = signer - self._scopes = _helpers.scopes_to_string(scopes) + self._scopes = util.scopes_to_string(scopes) self._private_key_id = private_key_id self.client_id = client_id self._user_agent = user_agent @@ -649,22 +650,9 @@ class _JWTAccessCredentials(ServiceAccountCredentials): return result def refresh(self, http): - """Refreshes the access_token. - - The HTTP object is unused since no request needs to be made to - get a new token, it can just be generated locally. - - Args: - http: unused HTTP object - """ self._refresh(None) - def _refresh(self, http): - """Refreshes the access_token. - - Args: - http: unused HTTP object - """ + def _refresh(self, http_request): self.access_token, self.token_expiry = self._create_token() def _create_token(self, additional_claims=None): diff --git a/oauth2client/tools.py b/oauth2client/tools.py index 5166993..8947157 100644 --- a/oauth2client/tools.py +++ b/oauth2client/tools.py @@ -30,10 +30,11 @@ from six.moves import http_client from six.moves import input from six.moves import urllib -from oauth2client import _helpers from oauth2client import client +from oauth2client import util +__author__ = 'jcgregorio@google.com (Joe Gregorio)' __all__ = ['argparser', 'run_flow', 'message_if_missing'] _CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0 @@ -92,7 +93,6 @@ def _CreateArgumentParser(): help='Set the logging level of detail.') return parser - # argparser is an ArgumentParser that contains command-line options expected # by tools.run(). Pass it in as part of the 'parents' argument to your own # ArgumentParser. @@ -123,22 +123,22 @@ class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler): if an error occurred. """ self.send_response(http_client.OK) - self.send_header('Content-type', 'text/html') + self.send_header("Content-type", "text/html") self.end_headers() - parts = urllib.parse.urlparse(self.path) - query = _helpers.parse_unique_urlencoded(parts.query) + query = self.path.split('?', 1)[-1] + query = dict(urllib.parse.parse_qsl(query)) self.server.query_params = query self.wfile.write( - b'Authentication Status') + b"Authentication Status") self.wfile.write( - b'

The authentication flow has completed.

') - self.wfile.write(b'') + b"

The authentication flow has completed.

") + self.wfile.write(b"") def log_message(self, format, *args): """Do not log messages to stdout while running as cmd. line program.""" -@_helpers.positional(3) +@util.positional(3) def run_flow(flow, storage, flags=None, http=None): """Core code for a command-line application. diff --git a/oauth2client/transport.py b/oauth2client/transport.py index 79a61f1..8dbc60d 100644 --- a/oauth2client/transport.py +++ b/oauth2client/transport.py @@ -18,7 +18,7 @@ import httplib2 import six from six.moves import http_client -from oauth2client import _helpers +from oauth2client._helpers import _to_bytes _LOGGER = logging.getLogger(__name__) @@ -58,19 +58,13 @@ def get_cached_http(): return _CACHED_HTTP -def get_http_object(*args, **kwargs): +def get_http_object(): """Return a new HTTP object. - Args: - *args: tuple, The positional arguments to be passed when - contructing a new HTTP object. - **kwargs: dict, The keyword arguments to be passed when - contructing a new HTTP object. - Returns: httplib2.Http, an HTTP object. """ - return httplib2.Http(*args, **kwargs) + return httplib2.Http() def _initialize_headers(headers): @@ -127,7 +121,7 @@ def clean_headers(headers): k = str(k) if not isinstance(v, six.binary_type): v = str(v) - clean[_helpers._to_bytes(k)] = _helpers._to_bytes(v) + clean[_to_bytes(k)] = _to_bytes(v) except UnicodeEncodeError: from oauth2client.client import NonAsciiHeaderError raise NonAsciiHeaderError(k, ': ', v) @@ -170,9 +164,9 @@ def wrap_http_for_auth(credentials, http): _STREAM_PROPERTIES): body_stream_position = body.tell() - resp, content = request(orig_request_method, uri, method, body, - clean_headers(headers), - redirections, connection_type) + resp, content = orig_request_method(uri, method, body, + clean_headers(headers), + redirections, connection_type) # A stored token may expire between the time it is retrieved and # the time the request is made, so we may need to try twice. @@ -188,9 +182,9 @@ def wrap_http_for_auth(credentials, http): if body_stream_position is not None: body.seek(body_stream_position) - resp, content = request(orig_request_method, uri, method, body, - clean_headers(headers), - redirections, connection_type) + resp, content = orig_request_method(uri, method, body, + clean_headers(headers), + redirections, connection_type) return resp, content @@ -198,7 +192,7 @@ def wrap_http_for_auth(credentials, http): http.request = new_request # Set credentials as a property of the request method. - http.request.credentials = credentials + setattr(http.request, 'credentials', credentials) def wrap_http_for_jwt_access(credentials, http): @@ -228,9 +222,9 @@ def wrap_http_for_jwt_access(credentials, http): if (credentials.access_token is None or credentials.access_token_expired): credentials.refresh(None) - return request(authenticated_request_method, uri, - method, body, headers, redirections, - connection_type) + return authenticated_request_method(uri, method, body, + headers, redirections, + connection_type) else: # If we don't have an 'aud' (audience) claim, # create a 1-time token with the uri root as the audience @@ -240,46 +234,12 @@ def wrap_http_for_jwt_access(credentials, http): token, unused_expiry = credentials._create_token({'aud': uri_root}) headers['Authorization'] = 'Bearer ' + token - return request(orig_request_method, uri, method, body, - clean_headers(headers), - redirections, connection_type) + return orig_request_method(uri, method, body, + clean_headers(headers), + redirections, connection_type) # Replace the request method with our own closure. http.request = new_request - # Set credentials as a property of the request method. - http.request.credentials = credentials - - -def request(http, uri, method='GET', body=None, headers=None, - redirections=httplib2.DEFAULT_MAX_REDIRECTS, - connection_type=None): - """Make an HTTP request with an HTTP object and arguments. - - Args: - http: httplib2.Http, an http object to be used to make requests. - uri: string, The URI to be requested. - method: string, The HTTP method to use for the request. Defaults - to 'GET'. - body: string, The payload / body in HTTP request. By default - there is no payload. - headers: dict, Key-value pairs of request headers. By default - there are no headers. - redirections: int, The number of allowed 203 redirects for - the request. Defaults to 5. - connection_type: httplib.HTTPConnection, a subclass to be used for - establishing connection. If not set, the type - will be determined from the ``uri``. - - Returns: - tuple, a pair of a httplib2.Response with the status code and other - headers and the bytes of the content returned. - """ - # NOTE: Allowing http or http.request is temporary (See Issue 601). - http_callable = getattr(http, 'request', http) - return http_callable(uri, method=method, body=body, headers=headers, - redirections=redirections, - connection_type=connection_type) - _CACHED_HTTP = httplib2.Http(MemoryCache()) diff --git a/oauth2client/util.py b/oauth2client/util.py new file mode 100644 index 0000000..e3ba62b --- /dev/null +++ b/oauth2client/util.py @@ -0,0 +1,206 @@ +# Copyright 2014 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Common utility library.""" + +import functools +import inspect +import logging + +import six +from six.moves import urllib + + +__author__ = [ + 'rafek@google.com (Rafe Kaplan)', + 'guido@google.com (Guido van Rossum)', +] + +__all__ = [ + 'positional', + 'POSITIONAL_WARNING', + 'POSITIONAL_EXCEPTION', + 'POSITIONAL_IGNORE', +] + +logger = logging.getLogger(__name__) + +POSITIONAL_WARNING = 'WARNING' +POSITIONAL_EXCEPTION = 'EXCEPTION' +POSITIONAL_IGNORE = 'IGNORE' +POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION, + POSITIONAL_IGNORE]) + +positional_parameters_enforcement = POSITIONAL_WARNING + + +def positional(max_positional_args): + """A decorator to declare that only the first N arguments my be positional. + + This decorator makes it easy to support Python 3 style keyword-only + parameters. For example, in Python 3 it is possible to write:: + + def fn(pos1, *, kwonly1=None, kwonly1=None): + ... + + All named parameters after ``*`` must be a keyword:: + + fn(10, 'kw1', 'kw2') # Raises exception. + fn(10, kwonly1='kw1') # Ok. + + Example + ^^^^^^^ + + To define a function like above, do:: + + @positional(1) + def fn(pos1, kwonly1=None, kwonly2=None): + ... + + If no default value is provided to a keyword argument, it becomes a + required keyword argument:: + + @positional(0) + def fn(required_kw): + ... + + This must be called with the keyword parameter:: + + fn() # Raises exception. + fn(10) # Raises exception. + fn(required_kw=10) # Ok. + + When defining instance or class methods always remember to account for + ``self`` and ``cls``:: + + class MyClass(object): + + @positional(2) + def my_method(self, pos1, kwonly1=None): + ... + + @classmethod + @positional(2) + def my_method(cls, pos1, kwonly1=None): + ... + + The positional decorator behavior is controlled by + ``util.positional_parameters_enforcement``, which may be set to + ``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or + ``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do + nothing, respectively, if a declaration is violated. + + Args: + max_positional_arguments: Maximum number of positional arguments. All + parameters after the this index must be + keyword only. + + Returns: + A decorator that prevents using arguments after max_positional_args + from being used as positional parameters. + + Raises: + TypeError: if a key-word only argument is provided as a positional + parameter, but only if + util.positional_parameters_enforcement is set to + POSITIONAL_EXCEPTION. + """ + + def positional_decorator(wrapped): + @functools.wraps(wrapped) + def positional_wrapper(*args, **kwargs): + if len(args) > max_positional_args: + plural_s = '' + if max_positional_args != 1: + plural_s = 's' + message = ('{function}() takes at most {args_max} positional ' + 'argument{plural} ({args_given} given)'.format( + function=wrapped.__name__, + args_max=max_positional_args, + args_given=len(args), + plural=plural_s)) + if positional_parameters_enforcement == POSITIONAL_EXCEPTION: + raise TypeError(message) + elif positional_parameters_enforcement == POSITIONAL_WARNING: + logger.warning(message) + return wrapped(*args, **kwargs) + return positional_wrapper + + if isinstance(max_positional_args, six.integer_types): + return positional_decorator + else: + args, _, _, defaults = inspect.getargspec(max_positional_args) + return positional(len(args) - len(defaults))(max_positional_args) + + +def scopes_to_string(scopes): + """Converts scope value to a string. + + If scopes is a string then it is simply passed through. If scopes is an + iterable then a string is returned that is all the individual scopes + concatenated with spaces. + + Args: + scopes: string or iterable of strings, the scopes. + + Returns: + The scopes formatted as a single string. + """ + if isinstance(scopes, six.string_types): + return scopes + else: + return ' '.join(scopes) + + +def string_to_scopes(scopes): + """Converts stringifed scope value to a list. + + If scopes is a list then it is simply passed through. If scopes is an + string then a list of each individual scope is returned. + + Args: + scopes: a string or iterable of strings, the scopes. + + Returns: + The scopes in a list. + """ + if not scopes: + return [] + if isinstance(scopes, six.string_types): + return scopes.split(' ') + else: + return scopes + + +def _add_query_parameter(url, name, value): + """Adds a query parameter to a url. + + Replaces the current value if it already exists in the URL. + + Args: + url: string, url to add the query parameter to. + name: string, query parameter name. + value: string, query parameter value. + + Returns: + Updated query parameter. Does not update the url if value is None. + """ + if value is None: + return url + else: + parsed = list(urllib.parse.urlparse(url)) + q = dict(urllib.parse.parse_qsl(parsed[4])) + q[name] = value + parsed[4] = urllib.parse.urlencode(q) + return urllib.parse.urlunparse(parsed) diff --git a/samples/django/README.md b/samples/django/README.md deleted file mode 100644 index 27c0fda..0000000 --- a/samples/django/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Django Samples - -These two sample Django apps provide a skeleton for the two main use cases of the -`oauth2client.contrib.django_util` helpers. - -Please see the -[core docs](https://oauth2client.readthedocs.io/en/latest/) for more information and usage examples. - -## google_user - -This is the simpler use case of the library. It assumes you are using Google OAuth as your primary -authorization and authentication mechanism for your application. Users log in with their Google ID -and their OAuth2 credentials are stored inside the session. - -## django_user - -This is the use case where the application is already using the Django authorization system and -has a Django model with a `django.contrib.auth.models.User` field, and would like to attach -a Google OAuth2 credentials object to that model. Users have to login, and then can login with -their Google account to associate the Google account with the user in the Django system. -Credentials will be stored in the Django ORM backend. diff --git a/samples/django/django_user/manage.py b/samples/django/django_user/manage.py deleted file mode 100755 index 1a30708..0000000 --- a/samples/django/django_user/manage.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python -# Copyright 2016 Google Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os -import sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myoauth.settings") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff --git a/samples/django/django_user/myoauth/__init__.py b/samples/django/django_user/myoauth/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/samples/django/django_user/myoauth/settings.py b/samples/django/django_user/myoauth/settings.py deleted file mode 100644 index 5ef2f99..0000000 --- a/samples/django/django_user/myoauth/settings.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -import os - -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'eiw+mvmua#98n@p2xq+c#liz@r2&#-s07nkgz)+$zcl^o4$-$o' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - -# Application definition - -INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'polls', - 'oauth2client.contrib.django_util', -) - -MIDDLEWARE_CLASSES = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', -) - -ROOT_URLCONF = 'myoauth.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'myoauth.wsgi.application' - -# Database -# https://docs.djangoproject.com/en/1.8/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } -} - -# Internationalization -# https://docs.djangoproject.com/en/1.8/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.8/howto/static-files/ - -STATIC_URL = '/static/' - -GOOGLE_OAUTH2_CLIENT_ID = 'YOUR_CLIENT_ID' - -GOOGLE_OAUTH2_CLIENT_SECRET = 'YOUR_CLIENT_SECRET' - -GOOGLE_OAUTH2_SCOPES = ( - 'email', 'profile') - -GOOGLE_OAUTH2_STORAGE_MODEL = { - 'model': 'polls.models.CredentialsModel', - 'user_property': 'user_id', - 'credentials_property': 'credential', -} - -LOGIN_URL = '/login' diff --git a/samples/django/django_user/myoauth/urls.py b/samples/django/django_user/myoauth/urls.py deleted file mode 100644 index 6636b4e..0000000 --- a/samples/django/django_user/myoauth/urls.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from django.conf import urls -from django.contrib import admin -import django.contrib.auth.views -from polls import views - -import oauth2client.contrib.django_util.site as django_util_site - - -urlpatterns = [ - urls.url(r'^$', views.index), - urls.url(r'^profile_required$', views.get_profile_required), - urls.url(r'^profile_enabled$', views.get_profile_optional), - urls.url(r'^admin/', urls.include(admin.site.urls)), - urls.url(r'^login', django.contrib.auth.views.login, name="login"), - urls.url(r'^oauth2/', urls.include(django_util_site.urls)), -] diff --git a/samples/django/django_user/myoauth/wsgi.py b/samples/django/django_user/myoauth/wsgi.py deleted file mode 100644 index 39ee648..0000000 --- a/samples/django/django_user/myoauth/wsgi.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myoauth.settings") - -application = get_wsgi_application() diff --git a/samples/django/django_user/polls/__init__.py b/samples/django/django_user/polls/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/samples/django/django_user/polls/models.py b/samples/django/django_user/polls/models.py deleted file mode 100644 index 563f66e..0000000 --- a/samples/django/django_user/polls/models.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from django.contrib.auth.models import User -from django.db import models - -from oauth2client.contrib.django_util.models import CredentialsField - - -class CredentialsModel(models.Model): - user_id = models.OneToOneField(User) - credential = CredentialsField() diff --git a/samples/django/django_user/polls/templates/registration/login.html b/samples/django/django_user/polls/templates/registration/login.html deleted file mode 100644 index c43450e..0000000 --- a/samples/django/django_user/polls/templates/registration/login.html +++ /dev/null @@ -1,45 +0,0 @@ - - -{% if form.errors %} -

Your username and password didn't match. Please try again.

-{% endif %} - -{% if next %} -{% if user.is_authenticated %} -

Your account doesn't have access to this page. To proceed, - please login with an account that has access.

-{% else %} -

Please login to see this page.

-{% endif %} -{% endif %} - -
- {% csrf_token %} - - - - - - - - - -
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
- - - -
diff --git a/samples/django/django_user/polls/views.py b/samples/django/django_user/polls/views.py deleted file mode 100644 index 5888330..0000000 --- a/samples/django/django_user/polls/views.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from django.http import HttpResponse - -from oauth2client.contrib.django_util import decorators - - -def index(request): - return HttpResponse("Hello world!") - - -@decorators.oauth_required -def get_profile_required(request): - resp, content = request.oauth.http.request( - 'https://www.googleapis.com/plus/v1/people/me') - return HttpResponse(content) - - -@decorators.oauth_enabled -def get_profile_optional(request): - if request.oauth.has_credentials(): - # this could be passed into a view - # request.oauth.http is also initialized - return HttpResponse('User email: {}'.format( - request.oauth.credentials.id_token['email'])) - else: - return HttpResponse( - 'Here is an OAuth Authorize link:Authorize' - .format(request.oauth.get_authorize_redirect())) diff --git a/samples/django/django_user/requirements.txt b/samples/django/django_user/requirements.txt deleted file mode 100644 index b42af1f..0000000 --- a/samples/django/django_user/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -Django==1.10.0 -oauth2client==3.0.0 -jsonpickle==0.9.3 diff --git a/samples/django/google_user/manage.py b/samples/django/google_user/manage.py deleted file mode 100755 index 1a30708..0000000 --- a/samples/django/google_user/manage.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python -# Copyright 2016 Google Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os -import sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myoauth.settings") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff --git a/samples/django/google_user/myoauth/__init__.py b/samples/django/google_user/myoauth/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/samples/django/google_user/myoauth/settings.py b/samples/django/google_user/myoauth/settings.py deleted file mode 100644 index e08661d..0000000 --- a/samples/django/google_user/myoauth/settings.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -import os - -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'eiw+mvmua#98n@p2xq+c#liz@r2&#-s07nkgz)+$zcl^o4$-$o' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - -# Application definition - -INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'polls', - 'oauth2client.contrib.django_util', -) - -MIDDLEWARE_CLASSES = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', -) - -ROOT_URLCONF = 'myoauth.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'myoauth.wsgi.application' - -# Database -# https://docs.djangoproject.com/en/1.8/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } -} - -# Internationalization -# https://docs.djangoproject.com/en/1.8/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.8/howto/static-files/ - -STATIC_URL = '/static/' - -GOOGLE_OAUTH2_CLIENT_ID = 'YOUR_CLIENT_ID' - -GOOGLE_OAUTH2_CLIENT_SECRET = 'YOUR_CLIENT_SECRET' - -GOOGLE_OAUTH2_SCOPES = ( - 'email', 'profile') diff --git a/samples/django/google_user/myoauth/urls.py b/samples/django/google_user/myoauth/urls.py deleted file mode 100644 index 4d3d0a1..0000000 --- a/samples/django/google_user/myoauth/urls.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from django.conf import urls -from polls import views - -import oauth2client.contrib.django_util.site as django_util_site - - -urlpatterns = [ - urls.url(r'^$', views.index), - urls.url(r'^profile_required$', views.get_profile_required), - urls.url(r'^profile_enabled$', views.get_profile_optional), - urls.url(r'^oauth2/', urls.include(django_util_site.urls)) -] diff --git a/samples/django/google_user/myoauth/wsgi.py b/samples/django/google_user/myoauth/wsgi.py deleted file mode 100644 index 39ee648..0000000 --- a/samples/django/google_user/myoauth/wsgi.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myoauth.settings") - -application = get_wsgi_application() diff --git a/samples/django/google_user/polls/__init__.py b/samples/django/google_user/polls/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/samples/django/google_user/polls/views.py b/samples/django/google_user/polls/views.py deleted file mode 100644 index e4b9119..0000000 --- a/samples/django/google_user/polls/views.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from django import http - -from oauth2client.contrib.django_util import decorators - - -def index(request): - return http.HttpResponse("Hello world!") - - -@decorators.oauth_required -def get_profile_required(request): - resp, content = request.oauth.http.request( - 'https://www.googleapis.com/plus/v1/people/me') - return http.HttpResponse(content) - - -@decorators.oauth_enabled -def get_profile_optional(request): - if request.oauth.has_credentials(): - # this could be passed into a view - # request.oauth.http is also initialized - return http.HttpResponse('User email: {}'.format( - request.oauth.credentials.id_token['email'])) - else: - return http.HttpResponse( - 'Here is an OAuth Authorize link:Authorize' - .format(request.oauth.get_authorize_redirect())) diff --git a/samples/django/google_user/requirements.txt b/samples/django/google_user/requirements.txt deleted file mode 100644 index b42af1f..0000000 --- a/samples/django/google_user/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -Django==1.10.0 -oauth2client==3.0.0 -jsonpickle==0.9.3 diff --git a/scripts/fetch_gae_sdk.py b/scripts/fetch_gae_sdk.py new file mode 100755 index 0000000..24a6db5 --- /dev/null +++ b/scripts/fetch_gae_sdk.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +"""Fetch the most recent GAE SDK and decompress it in the current directory. + +Usage: + fetch_gae_sdk.py [] + +Current releases are listed here: + https://www.googleapis.com/storage/v1/b/appengine-sdks/o?prefix=featured +""" +from __future__ import print_function + +import json +import os +import StringIO +import sys +import urllib2 +import zipfile + + +_SDK_URL = ( + 'https://www.googleapis.com/storage/v1/b/appengine-sdks/o?prefix=featured') + + +def get_gae_versions(): + try: + version_info_json = urllib2.urlopen(_SDK_URL).read() + except: + return {} + try: + version_info = json.loads(version_info_json) + except: + return {} + return version_info.get('items', {}) + + +def _version_tuple(v): + version_string = os.path.splitext(v['name'])[0].rpartition('_')[2] + return tuple(int(x) for x in version_string.split('.')) + + +def get_sdk_urls(sdk_versions): + python_releases = [v for v in sdk_versions + if v['name'].startswith('featured/google_appengine')] + current_releases = sorted(python_releases, key=_version_tuple, + reverse=True) + return [release['mediaLink'] for release in current_releases] + + +def main(argv): + if len(argv) > 2: + print('Usage: {0} []'.format(argv[0])) + return 1 + dest_dir = argv[1] if len(argv) > 1 else '.' + if not os.path.exists(dest_dir): + os.makedirs(dest_dir) + + if os.path.exists(os.path.join(dest_dir, 'google_appengine')): + print('GAE SDK already installed at {0}, exiting.'.format(dest_dir)) + return 0 + + sdk_versions = get_gae_versions() + if not sdk_versions: + print('Error fetching GAE SDK version info') + return 1 + sdk_urls = get_sdk_urls(sdk_versions) + for sdk_url in sdk_urls: + try: + sdk_contents = StringIO.StringIO(urllib2.urlopen(sdk_url).read()) + break + except: + pass + else: + print('Could not read SDK from any of ', sdk_urls) + return 1 + sdk_contents.seek(0) + try: + zip_contents = zipfile.ZipFile(sdk_contents) + zip_contents.extractall(dest_dir) + except: + print('Error extracting SDK contents') + return 1 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[:])) diff --git a/scripts/install.sh b/scripts/install.sh index e1ed5c5..0ef7ad2 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -16,18 +16,16 @@ set -ev -pip install --upgrade pip setuptools tox coveralls - -# App Engine tests require the App Engine SDK. -if [[ "${TOX_ENV}" == "gae" || "${TOX_ENV}" == "cover" ]]; then - pip install gcp-devrel-py-tools - gcp-devrel-py-tools download-appengine-sdk `dirname ${GAE_PYTHONPATH}` +pip install tox +if [[ "${TOX_ENV}" == "pypy" ]]; then + git clone https://github.com/yyuu/pyenv.git ${HOME}/.pyenv + PYENV_ROOT="${HOME}/.pyenv" + PATH="${PYENV_ROOT}/bin:${PATH}" + eval "$(pyenv init -)" + pyenv install pypy-2.6.0 + pyenv global pypy-2.6.0 fi -# Travis ships with an old version of PyPy, so install at least version 2.6. -if [[ "${TOX_ENV}" == "pypy" ]]; then - if [ ! -d "${HOME}/.pyenv/bin" ]; then - git clone https://github.com/yyuu/pyenv.git ${HOME}/.pyenv - fi - ${HOME}/.pyenv/bin/pyenv install --skip-existing pypy-2.6.0 +if [[ "${TOX_ENV}" == "gae" && ! -d ${GAE_PYTHONPATH} ]]; then + python scripts/fetch_gae_sdk.py `dirname ${GAE_PYTHONPATH}` fi diff --git a/scripts/run.sh b/scripts/run.sh index c774f24..0b537e2 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -16,11 +16,10 @@ set -ev -# If in the pypy environment, activate the never version of pypy provided by -# pyenv. if [[ "${TOX_ENV}" == "pypy" ]]; then - PATH="${HOME}/.pyenv/versions/pypy-2.6.0/bin:${PATH}" - export PATH + PYENV_ROOT="${HOME}/.pyenv" + PATH="${PYENV_ROOT}/bin:${PATH}" + eval "$(pyenv init -)" + pyenv global pypy-2.6.0 fi - tox -e ${TOX_ENV} diff --git a/scripts/run_gce_system_tests.py b/scripts/run_gce_system_tests.py index 80794bd..d446f9c 100644 --- a/scripts/run_gce_system_tests.py +++ b/scripts/run_gce_system_tests.py @@ -13,26 +13,26 @@ # limitations under the License. import json -import unittest +import httplib2 from six.moves import http_client from six.moves import urllib +import unittest2 -import oauth2client -from oauth2client import client -from oauth2client import transport -from oauth2client.contrib import gce +from oauth2client import GOOGLE_TOKEN_INFO_URI +from oauth2client.client import GoogleCredentials +from oauth2client.contrib.gce import AppAssertionCredentials -class TestComputeEngine(unittest.TestCase): +class TestComputeEngine(unittest2.TestCase): def test_application_default(self): - default_creds = client.GoogleCredentials.get_application_default() - self.assertIsInstance(default_creds, gce.AppAssertionCredentials) + default_creds = GoogleCredentials.get_application_default() + self.assertIsInstance(default_creds, AppAssertionCredentials) def test_token_info(self): - credentials = gce.AppAssertionCredentials([]) - http = transport.get_http_object() + credentials = AppAssertionCredentials([]) + http = httplib2.Http() # First refresh to get the access token. self.assertIsNone(credentials.access_token) @@ -41,9 +41,9 @@ class TestComputeEngine(unittest.TestCase): # Then check the access token against the token info API. query_params = {'access_token': credentials.access_token} - token_uri = (oauth2client.GOOGLE_TOKEN_INFO_URI + '?' + + token_uri = (GOOGLE_TOKEN_INFO_URI + '?' + urllib.parse.urlencode(query_params)) - response, content = transport.request(http, token_uri) + response, content = http.request(token_uri) self.assertEqual(response.status, http_client.OK) content = content.decode('utf-8') @@ -53,4 +53,4 @@ class TestComputeEngine(unittest.TestCase): if __name__ == '__main__': - unittest.main() + unittest2.main() diff --git a/scripts/run_system_tests.py b/scripts/run_system_tests.py index 4c9c80c..ce99e7c 100644 --- a/scripts/run_system_tests.py +++ b/scripts/run_system_tests.py @@ -15,12 +15,12 @@ import json import os +import httplib2 from six.moves import http_client import oauth2client from oauth2client import client -from oauth2client import service_account -from oauth2client import transport +from oauth2client.service_account import ServiceAccountCredentials JSON_KEY_PATH = os.getenv('OAUTH2CLIENT_TEST_JSON_KEY_PATH') @@ -56,8 +56,8 @@ def _require_environ(): def _check_user_info(credentials, expected_email): - http = credentials.authorize(transport.get_http_object()) - response, content = transport.request(http, USER_INFO) + http = credentials.authorize(httplib2.Http()) + response, content = http.request(USER_INFO) if response.status != http_client.OK: raise ValueError('Expected 200 OK response.') @@ -68,14 +68,14 @@ def _check_user_info(credentials, expected_email): def run_json(): - factory = service_account.ServiceAccountCredentials.from_json_keyfile_name - credentials = factory(JSON_KEY_PATH, scopes=SCOPE) + credentials = ServiceAccountCredentials.from_json_keyfile_name( + JSON_KEY_PATH, scopes=SCOPE) service_account_email = credentials._service_account_email _check_user_info(credentials, service_account_email) def run_p12(): - credentials = service_account.ServiceAccountCredentials.from_p12_keyfile( + credentials = ServiceAccountCredentials.from_p12_keyfile( P12_KEY_EMAIL, P12_KEY_PATH, scopes=SCOPE) _check_user_info(credentials, P12_KEY_EMAIL) diff --git a/scripts/run_system_tests.sh b/scripts/run_system_tests.sh index 2e10e5c..7169eb7 100755 --- a/scripts/run_system_tests.sh +++ b/scripts/run_system_tests.sh @@ -19,9 +19,10 @@ set -ev # If we're on Travis, we need to set up the environment. if [[ "${TRAVIS}" == "true" ]]; then - # If secure variables are available, run system test. - if [[ "${TRAVIS_SECURE_ENV_VARS}" ]]; then - echo "Running in Travis, decrypting stored key file." + # If merging to master and not a pull request, run system test. + if [[ "${TRAVIS_BRANCH}" == "master" ]] && \ + [[ "${TRAVIS_PULL_REQUEST}" == "false" ]]; then + echo "Running in Travis during merge, decrypting stored key file." # Convert encrypted JSON key file into decrypted file to be used. openssl aes-256-cbc -K ${OAUTH2CLIENT_KEY} \ -iv ${OAUTH2CLIENT_IV} \ @@ -33,8 +34,8 @@ if [[ "${TRAVIS}" == "true" ]]; then -in tests/data/key.p12.enc \ -out ${OAUTH2CLIENT_TEST_P12_KEY_PATH} -d # Convert encrypted User JSON key file into decrypted file to be used. - openssl aes-256-cbc -K ${encrypted_1ee98544e5ca_key} \ - -iv ${encrypted_1ee98544e5ca_iv} \ + openssl aes-256-cbc -K ${OAUTH2CLIENT_KEY} \ + -iv ${OAUTH2CLIENT_IV} \ -in tests/data/user-key.json.enc \ -out ${OAUTH2CLIENT_TEST_USER_KEY_PATH} -d else diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2a9acf1..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal = 1 diff --git a/setup.py b/setup.py index 47d86b7..686d1db 100644 --- a/setup.py +++ b/setup.py @@ -26,11 +26,11 @@ from setuptools import setup import oauth2client -if sys.version_info < (2, 7): - print('oauth2client requires python2 version >= 2.7.', file=sys.stderr) +if sys.version_info < (2, 6): + print('oauth2client requires python2 version >= 2.6.', file=sys.stderr) sys.exit(1) -if (3, 1) <= sys.version_info < (3, 4): - print('oauth2client requires python3 version >= 3.4.', file=sys.stderr) +if (3, 1) <= sys.version_info < (3, 3): + print('oauth2client requires python3 version >= 3.3.', file=sys.stderr) sys.exit(1) install_requires = [ @@ -41,36 +41,29 @@ install_requires = [ 'six>=1.6.1', ] -long_desc = """ -oauth2client is a client library for OAuth 2.0. - -Note: oauth2client is now deprecated. No more features will be added to the - libraries and the core team is turning down support. We recommend you use - `google-auth `__ and - `oauthlib `__. -""" +long_desc = """The oauth2client is a client library for OAuth 2.0.""" version = oauth2client.__version__ setup( - name='oauth2client', + name="oauth2client", version=version, - description='OAuth 2.0 client library', + description="OAuth 2.0 client library", long_description=long_desc, - author='Google Inc.', - author_email='jonwayne+oauth2client@google.com', - url='http://github.com/google/oauth2client/', + author="Google Inc.", + url="http://github.com/google/oauth2client/", install_requires=install_requires, - packages=find_packages(exclude=('tests*',)), - license='Apache 2.0', - keywords='google oauth 2.0 http client', + packages=find_packages(), + license="Apache 2.0", + keywords="google oauth 2.0 http client", classifiers=[ 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Development Status :: 7 - Inactive', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Operating System :: POSIX', diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..5f6567c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,22 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test package set-up.""" + +from oauth2client import util + +__author__ = 'afshar@google.com (Ali Afshar)' + + +def setup_package(): + """Run on testing package.""" + util.positional_parameters_enforcement = util.POSITIONAL_EXCEPTION diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index caadb80..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Py.test hooks.""" - -from oauth2client import _helpers - - -def pytest_addoption(parser): - """Adds the --gae-sdk option to py.test. - - This is used to enable the GAE tests. This has to be in this conftest.py - due to the way py.test collects conftest files.""" - parser.addoption('--gae-sdk') - - -def pytest_configure(config): - """Py.test hook called before loading tests.""" - # Default of POSITIONAL_WARNING is too verbose for testing - _helpers.positional_parameters_enforcement = _helpers.POSITIONAL_EXCEPTION diff --git a/tests/contrib/appengine/__init__.py b/tests/contrib/appengine/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/contrib/appengine/conftest.py b/tests/contrib/appengine/conftest.py deleted file mode 100644 index b56fbcd..0000000 --- a/tests/contrib/appengine/conftest.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""App Engine py.test configuration.""" - -import sys - -from six.moves import reload_module - - -def set_up_gae_environment(sdk_path): - """Set up appengine SDK third-party imports. - - The App Engine SDK does terrible things to the global interpreter state. - Because of this, this stuff can't be neatly undone. As such, it can't be - a fixture. - """ - if 'google' in sys.modules: - # Some packages, such as protobuf, clobber the google - # namespace package. This prevents that. - reload_module(sys.modules['google']) - - # This sets up google-provided libraries. - sys.path.insert(0, sdk_path) - import dev_appserver - dev_appserver.fix_sys_path() - - # Fixes timezone and other os-level items. - import google.appengine.tools.os_compat # noqa: unused import - - -def pytest_configure(config): - """Configures the App Engine SDK imports on py.test startup.""" - if config.getoption('gae_sdk') is not None: - set_up_gae_environment(config.getoption('gae_sdk')) - - -def pytest_ignore_collect(path, config): - """Skip App Engine tests when --gae-sdk is not specified.""" - return ( - 'contrib/appengine' in str(path) and - config.getoption('gae_sdk') is None) diff --git a/tests/contrib/appengine/test__appengine_ndb.py b/tests/contrib/appengine/test__appengine_ndb.py deleted file mode 100644 index 9af1dcc..0000000 --- a/tests/contrib/appengine/test__appengine_ndb.py +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import os -import unittest - -from google.appengine.ext import ndb -from google.appengine.ext import testbed -import mock - -from oauth2client import client -from oauth2client.contrib import appengine - - -DATA_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'data') - - -def datafile(filename): - return os.path.join(DATA_DIR, filename) - - -class TestNDBModel(ndb.Model): - flow = appengine.FlowNDBProperty() - creds = appengine.CredentialsNDBProperty() - - -class TestFlowNDBProperty(unittest.TestCase): - - def setUp(self): - self.testbed = testbed.Testbed() - self.testbed.activate() - self.testbed.init_datastore_v3_stub() - self.testbed.init_memcache_stub() - - def tearDown(self): - self.testbed.deactivate() - - def test_flow_get_put(self): - instance = TestNDBModel( - flow=client.flow_from_clientsecrets( - datafile('client_secrets.json'), 'foo', redirect_uri='oob'), - id='foo' - ) - instance.put() - retrieved = TestNDBModel.get_by_id('foo') - - self.assertEqual('foo_client_id', retrieved.flow.client_id) - - @mock.patch('oauth2client.contrib._appengine_ndb._LOGGER') - def test_validate_success(self, mock_logger): - flow_prop = TestNDBModel.flow - flow_val = client.flow_from_clientsecrets( - datafile('client_secrets.json'), 'foo', redirect_uri='oob') - flow_prop._validate(flow_val) - mock_logger.info.assert_called_once_with('validate: Got type %s', - type(flow_val)) - - @mock.patch('oauth2client.contrib._appengine_ndb._LOGGER') - def test_validate_none(self, mock_logger): - flow_prop = TestNDBModel.flow - flow_val = None - flow_prop._validate(flow_val) - mock_logger.info.assert_called_once_with('validate: Got type %s', - type(flow_val)) - - @mock.patch('oauth2client.contrib._appengine_ndb._LOGGER') - def test_validate_bad_type(self, mock_logger): - flow_prop = TestNDBModel.flow - flow_val = object() - with self.assertRaises(TypeError): - flow_prop._validate(flow_val) - mock_logger.info.assert_called_once_with('validate: Got type %s', - type(flow_val)) - - -class TestCredentialsNDBProperty(unittest.TestCase): - - def setUp(self): - self.testbed = testbed.Testbed() - self.testbed.activate() - self.testbed.init_datastore_v3_stub() - self.testbed.init_memcache_stub() - - def tearDown(self): - self.testbed.deactivate() - - def test_valid_creds_get_put(self): - creds = client.Credentials() - instance = TestNDBModel(creds=creds, id='bar') - instance.put() - retrieved = TestNDBModel.get_by_id('bar') - self.assertIsInstance(retrieved.creds, client.Credentials) - - @mock.patch('oauth2client.contrib._appengine_ndb._LOGGER') - def test_validate_success(self, mock_logger): - creds_prop = TestNDBModel.creds - creds_val = client.Credentials() - creds_prop._validate(creds_val) - mock_logger.info.assert_called_once_with('validate: Got type %s', - type(creds_val)) - - @mock.patch('oauth2client.contrib._appengine_ndb._LOGGER') - def test_validate_none(self, mock_logger): - creds_prop = TestNDBModel.creds - creds_val = None - creds_prop._validate(creds_val) - mock_logger.info.assert_called_once_with('validate: Got type %s', - type(creds_val)) - - @mock.patch('oauth2client.contrib._appengine_ndb._LOGGER') - def test_validate_bad_type(self, mock_logger): - creds_prop = TestNDBModel.creds - creds_val = object() - with self.assertRaises(TypeError): - creds_prop._validate(creds_val) - mock_logger.info.assert_called_once_with('validate: Got type %s', - type(creds_val)) - - def test__to_base_type_valid_creds(self): - creds_prop = TestNDBModel.creds - creds = client.Credentials() - creds_json = json.loads(creds_prop._to_base_type(creds)) - self.assertDictEqual(creds_json, { - '_class': 'Credentials', - '_module': 'oauth2client.client', - 'token_expiry': None, - }) - - def test__to_base_type_null_creds(self): - creds_prop = TestNDBModel.creds - self.assertEqual(creds_prop._to_base_type(None), '') - - def test__from_base_type_valid_creds(self): - creds_prop = TestNDBModel.creds - creds_json = json.dumps({ - '_class': 'Credentials', - '_module': 'oauth2client.client', - 'token_expiry': None, - }) - creds = creds_prop._from_base_type(creds_json) - self.assertIsInstance(creds, client.Credentials) - - def test__from_base_type_false_value(self): - creds_prop = TestNDBModel.creds - self.assertIsNone(creds_prop._from_base_type('')) - self.assertIsNone(creds_prop._from_base_type(False)) - self.assertIsNone(creds_prop._from_base_type(None)) - self.assertIsNone(creds_prop._from_base_type([])) - self.assertIsNone(creds_prop._from_base_type({})) - - def test__from_base_type_bad_json(self): - creds_prop = TestNDBModel.creds - creds_json = '{JK-I-AM-NOT-JSON' - self.assertIsNone(creds_prop._from_base_type(creds_json)) diff --git a/tests/contrib/appengine/test_appengine.py b/tests/contrib/appengine/test_appengine.py deleted file mode 100644 index 36d2713..0000000 --- a/tests/contrib/appengine/test_appengine.py +++ /dev/null @@ -1,1120 +0,0 @@ -# Copyright 2014 Google Inc. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import datetime -import json -import os -import tempfile -import time -import unittest - -from google.appengine.api import apiproxy_stub -from google.appengine.api import apiproxy_stub_map -from google.appengine.api import app_identity -from google.appengine.api import memcache -from google.appengine.api import users -from google.appengine.api.memcache import memcache_stub -from google.appengine.ext import db -from google.appengine.ext import ndb -from google.appengine.ext import testbed -import mock -from six.moves import urllib -from six.moves import urllib_parse -import webapp2 -from webtest import TestApp - -import oauth2client -from oauth2client import client -from oauth2client import clientsecrets -from oauth2client.contrib import appengine -from tests import http_mock - - -DATA_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'data') -DEFAULT_RESP = """\ -{ - "access_token": "foo_access_token", - "expires_in": 3600, - "extra": "value", - "refresh_token": "foo_refresh_token" -} -""" -BASIC_TOKEN = 'bar' -BASIC_RESP = json.dumps({'access_token': BASIC_TOKEN}) - - -def datafile(filename): - return os.path.join(DATA_DIR, filename) - - -def load_and_cache(existing_file, fakename, cache_mock): - client_type, client_info = clientsecrets._loadfile(datafile(existing_file)) - cache_mock.cache[fakename] = {client_type: client_info} - - -class UserMock(object): - """Mock the app engine user service""" - - def __call__(self): - return self - - def user_id(self): - return 'foo_user' - - -class UserNotLoggedInMock(object): - """Mock the app engine user service""" - - def __call__(self): - return None - - -class TestAppAssertionCredentials(unittest.TestCase): - account_name = "service_account_name@appspot.com" - signature = "signature" - - class AppIdentityStubImpl(apiproxy_stub.APIProxyStub): - - def __init__(self, key_name=None, sig_bytes=None, - svc_acct=None): - super(TestAppAssertionCredentials.AppIdentityStubImpl, - self).__init__('app_identity_service') - self._key_name = key_name - self._sig_bytes = sig_bytes - self._sign_calls = [] - self._svc_acct = svc_acct - self._get_acct_name_calls = 0 - - def _Dynamic_GetAccessToken(self, request, response): - response.set_access_token('a_token_123') - response.set_expiration_time(time.time() + 1800) - - def _Dynamic_SignForApp(self, request, response): - response.set_key_name(self._key_name) - response.set_signature_bytes(self._sig_bytes) - self._sign_calls.append(request.bytes_to_sign()) - - def _Dynamic_GetServiceAccountName(self, request, response): - response.set_service_account_name(self._svc_acct) - self._get_acct_name_calls += 1 - - class ErroringAppIdentityStubImpl(apiproxy_stub.APIProxyStub): - - def __init__(self): - super(TestAppAssertionCredentials.ErroringAppIdentityStubImpl, - self).__init__('app_identity_service') - - def _Dynamic_GetAccessToken(self, request, response): - raise app_identity.BackendDeadlineExceeded() - - def test_raise_correct_type_of_exception(self): - app_identity_stub = self.ErroringAppIdentityStubImpl() - apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap() - apiproxy_stub_map.apiproxy.RegisterStub('app_identity_service', - app_identity_stub) - apiproxy_stub_map.apiproxy.RegisterStub( - 'memcache', memcache_stub.MemcacheServiceStub()) - - scope = 'http://www.googleapis.com/scope' - credentials = appengine.AppAssertionCredentials(scope) - http = http_mock.HttpMock(data=DEFAULT_RESP) - with self.assertRaises(client.AccessTokenRefreshError): - credentials.refresh(http) - - def test_get_access_token_on_refresh(self): - app_identity_stub = self.AppIdentityStubImpl() - apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap() - apiproxy_stub_map.apiproxy.RegisterStub("app_identity_service", - app_identity_stub) - apiproxy_stub_map.apiproxy.RegisterStub( - 'memcache', memcache_stub.MemcacheServiceStub()) - - scope = [ - "http://www.googleapis.com/scope", - "http://www.googleapis.com/scope2"] - credentials = appengine.AppAssertionCredentials(scope) - http = http_mock.HttpMock(data=DEFAULT_RESP) - credentials.refresh(http) - self.assertEqual('a_token_123', credentials.access_token) - - json = credentials.to_json() - credentials = client.Credentials.new_from_json(json) - self.assertEqual( - 'http://www.googleapis.com/scope http://www.googleapis.com/scope2', - credentials.scope) - - scope = ('http://www.googleapis.com/scope ' - 'http://www.googleapis.com/scope2') - credentials = appengine.AppAssertionCredentials(scope) - http = http_mock.HttpMock(data=DEFAULT_RESP) - credentials.refresh(http) - self.assertEqual('a_token_123', credentials.access_token) - self.assertEqual( - 'http://www.googleapis.com/scope http://www.googleapis.com/scope2', - credentials.scope) - - def test_custom_service_account(self): - scope = "http://www.googleapis.com/scope" - account_id = "service_account_name_2@appspot.com" - - with mock.patch.object(app_identity, 'get_access_token', - return_value=('a_token_456', None), - autospec=True) as get_access_token: - credentials = appengine.AppAssertionCredentials( - scope, service_account_id=account_id) - http = http_mock.HttpMock(data=DEFAULT_RESP) - credentials.refresh(http) - - self.assertEqual('a_token_456', credentials.access_token) - self.assertEqual(scope, credentials.scope) - get_access_token.assert_called_once_with( - [scope], service_account_id=account_id) - - def test_create_scoped_required_without_scopes(self): - credentials = appengine.AppAssertionCredentials([]) - self.assertTrue(credentials.create_scoped_required()) - - def test_create_scoped_required_with_scopes(self): - credentials = appengine.AppAssertionCredentials(['dummy_scope']) - self.assertFalse(credentials.create_scoped_required()) - - def test_create_scoped(self): - credentials = appengine.AppAssertionCredentials([]) - new_credentials = credentials.create_scoped(['dummy_scope']) - self.assertNotEqual(credentials, new_credentials) - self.assertIsInstance( - new_credentials, appengine.AppAssertionCredentials) - self.assertEqual('dummy_scope', new_credentials.scope) - - def test_sign_blob(self): - key_name = b'1234567890' - sig_bytes = b'himom' - app_identity_stub = self.AppIdentityStubImpl( - key_name=key_name, sig_bytes=sig_bytes) - apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap() - apiproxy_stub_map.apiproxy.RegisterStub('app_identity_service', - app_identity_stub) - credentials = appengine.AppAssertionCredentials([]) - to_sign = b'blob' - self.assertEqual(app_identity_stub._sign_calls, []) - result = credentials.sign_blob(to_sign) - self.assertEqual(result, (key_name, sig_bytes)) - self.assertEqual(app_identity_stub._sign_calls, [to_sign]) - - def test_service_account_email(self): - acct_name = 'new-value@appspot.gserviceaccount.com' - app_identity_stub = self.AppIdentityStubImpl(svc_acct=acct_name) - apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap() - apiproxy_stub_map.apiproxy.RegisterStub('app_identity_service', - app_identity_stub) - - credentials = appengine.AppAssertionCredentials([]) - self.assertIsNone(credentials._service_account_email) - self.assertEqual(app_identity_stub._get_acct_name_calls, 0) - self.assertEqual(credentials.service_account_email, acct_name) - self.assertIsNotNone(credentials._service_account_email) - self.assertEqual(app_identity_stub._get_acct_name_calls, 1) - - def test_service_account_email_already_set(self): - acct_name = 'existing@appspot.gserviceaccount.com' - credentials = appengine.AppAssertionCredentials([]) - credentials._service_account_email = acct_name - - app_identity_stub = self.AppIdentityStubImpl(svc_acct=acct_name) - apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap() - apiproxy_stub_map.apiproxy.RegisterStub('app_identity_service', - app_identity_stub) - - self.assertEqual(app_identity_stub._get_acct_name_calls, 0) - self.assertEqual(credentials.service_account_email, acct_name) - self.assertEqual(app_identity_stub._get_acct_name_calls, 0) - - def test_get_access_token(self): - app_identity_stub = self.AppIdentityStubImpl() - apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap() - apiproxy_stub_map.apiproxy.RegisterStub("app_identity_service", - app_identity_stub) - apiproxy_stub_map.apiproxy.RegisterStub( - 'memcache', memcache_stub.MemcacheServiceStub()) - - credentials = appengine.AppAssertionCredentials(['dummy_scope']) - token = credentials.get_access_token() - self.assertEqual('a_token_123', token.access_token) - self.assertEqual(None, token.expires_in) - - def test_save_to_well_known_file(self): - os.environ[client._CLOUDSDK_CONFIG_ENV_VAR] = tempfile.mkdtemp() - credentials = appengine.AppAssertionCredentials([]) - with self.assertRaises(NotImplementedError): - client.save_to_well_known_file(credentials) - del os.environ[client._CLOUDSDK_CONFIG_ENV_VAR] - - -class TestFlowModel(db.Model): - flow = appengine.FlowProperty() - - -class FlowPropertyTest(unittest.TestCase): - - def setUp(self): - self.testbed = testbed.Testbed() - self.testbed.activate() - self.testbed.init_datastore_v3_stub() - - self.flow = client.flow_from_clientsecrets( - datafile('client_secrets.json'), - 'foo', - redirect_uri='oob') - - def tearDown(self): - self.testbed.deactivate() - - def test_flow_get_put(self): - instance = TestFlowModel( - flow=self.flow, - key_name='foo' - ) - instance.put() - retrieved = TestFlowModel.get_by_key_name('foo') - - self.assertEqual('foo_client_id', retrieved.flow.client_id) - - def test_make_value_from_datastore_none(self): - self.assertIsNone( - appengine.FlowProperty().make_value_from_datastore(None)) - - def test_validate(self): - appengine.FlowProperty().validate(None) - with self.assertRaises(db.BadValueError): - appengine.FlowProperty().validate(42) - - -class TestCredentialsModel(db.Model): - credentials = appengine.CredentialsProperty() - - -class CredentialsPropertyTest(unittest.TestCase): - - def setUp(self): - self.testbed = testbed.Testbed() - self.testbed.activate() - self.testbed.init_datastore_v3_stub() - - access_token = 'foo' - client_id = 'some_client_id' - client_secret = 'cOuDdkfjxxnv+' - refresh_token = '1/0/a.df219fjls0' - token_expiry = datetime.datetime.utcnow() - user_agent = 'refresh_checker/1.0' - self.credentials = client.OAuth2Credentials( - access_token, client_id, client_secret, - refresh_token, token_expiry, oauth2client.GOOGLE_TOKEN_URI, - user_agent) - - def tearDown(self): - self.testbed.deactivate() - - def test_credentials_get_put(self): - instance = TestCredentialsModel( - credentials=self.credentials, - key_name='foo' - ) - instance.put() - retrieved = TestCredentialsModel.get_by_key_name('foo') - - self.assertEqual( - self.credentials.to_json(), - retrieved.credentials.to_json()) - - def test_make_value_from_datastore(self): - self.assertIsNone( - appengine.CredentialsProperty().make_value_from_datastore(None)) - self.assertIsNone( - appengine.CredentialsProperty().make_value_from_datastore('')) - self.assertIsNone( - appengine.CredentialsProperty().make_value_from_datastore('{')) - - decoded = appengine.CredentialsProperty().make_value_from_datastore( - self.credentials.to_json()) - self.assertEqual( - self.credentials.to_json(), - decoded.to_json()) - - def test_validate(self): - appengine.CredentialsProperty().validate(self.credentials) - appengine.CredentialsProperty().validate(None) - with self.assertRaises(db.BadValueError): - appengine.CredentialsProperty().validate(42) - - -class StorageByKeyNameTest(unittest.TestCase): - - def setUp(self): - self.testbed = testbed.Testbed() - self.testbed.activate() - self.testbed.init_datastore_v3_stub() - self.testbed.init_memcache_stub() - self.testbed.init_user_stub() - - access_token = 'foo' - client_id = 'some_client_id' - client_secret = 'cOuDdkfjxxnv+' - refresh_token = '1/0/a.df219fjls0' - token_expiry = datetime.datetime.utcnow() - user_agent = 'refresh_checker/1.0' - self.credentials = client.OAuth2Credentials( - access_token, client_id, client_secret, - refresh_token, token_expiry, oauth2client.GOOGLE_TOKEN_URI, - user_agent) - - def tearDown(self): - self.testbed.deactivate() - - def test_bad_ctor(self): - with self.assertRaises(ValueError): - appengine.StorageByKeyName(appengine.CredentialsModel, None, None) - - def test__is_ndb(self): - storage = appengine.StorageByKeyName( - object(), 'foo', 'credentials') - - with self.assertRaises(TypeError): - storage._is_ndb() - - storage._model = type(object) - with self.assertRaises(TypeError): - storage._is_ndb() - - storage._model = appengine.CredentialsModel - self.assertFalse(storage._is_ndb()) - - storage._model = appengine.CredentialsNDBModel - self.assertTrue(storage._is_ndb()) - - def _verify_basic_refresh(self, http): - self.assertEqual(http.requests, 1) - self.assertEqual(http.uri, oauth2client.GOOGLE_TOKEN_URI) - self.assertEqual(http.method, 'POST') - expected_body = { - 'grant_type': ['refresh_token'], - 'client_id': [self.credentials.client_id], - 'client_secret': [self.credentials.client_secret], - 'refresh_token': [self.credentials.refresh_token], - } - self.assertEqual(urllib_parse.parse_qs(http.body), expected_body) - expected_headers = { - 'content-type': 'application/x-www-form-urlencoded', - 'user-agent': self.credentials.user_agent, - } - self.assertEqual(http.headers, expected_headers) - - def test_get_and_put_simple(self): - storage = appengine.StorageByKeyName( - appengine.CredentialsModel, 'foo', 'credentials') - - self.assertEqual(None, storage.get()) - self.credentials.set_store(storage) - - http = http_mock.HttpMock(data=BASIC_RESP) - self.credentials._refresh(http) - credmodel = appengine.CredentialsModel.get_by_key_name('foo') - self.assertEqual(BASIC_TOKEN, credmodel.credentials.access_token) - # Verify mock. - self._verify_basic_refresh(http) - - def test_get_and_put_cached(self): - storage = appengine.StorageByKeyName( - appengine.CredentialsModel, 'foo', 'credentials', cache=memcache) - - self.assertEqual(None, storage.get()) - self.credentials.set_store(storage) - - http = http_mock.HttpMock(data=BASIC_RESP) - self.credentials._refresh(http) - credmodel = appengine.CredentialsModel.get_by_key_name('foo') - self.assertEqual(BASIC_TOKEN, credmodel.credentials.access_token) - - # Now remove the item from the cache. - memcache.delete('foo') - - # Check that getting refreshes the cache. - credentials = storage.get() - self.assertEqual(BASIC_TOKEN, credentials.access_token) - self.assertNotEqual(None, memcache.get('foo')) - - # Deleting should clear the cache. - storage.delete() - credentials = storage.get() - self.assertEqual(None, credentials) - self.assertEqual(None, memcache.get('foo')) - - # Verify mock. - self._verify_basic_refresh(http) - - def test_get_and_put_set_store_on_cache_retrieval(self): - storage = appengine.StorageByKeyName( - appengine.CredentialsModel, 'foo', 'credentials', cache=memcache) - - self.assertEqual(None, storage.get()) - self.credentials.set_store(storage) - storage.put(self.credentials) - # Pre-bug 292 old_creds wouldn't have storage, and the _refresh - # wouldn't be able to store the updated cred back into the storage. - old_creds = storage.get() - self.assertEqual(old_creds.access_token, 'foo') - old_creds.invalid = True - http = http_mock.HttpMock(data=BASIC_RESP) - old_creds._refresh(http) - new_creds = storage.get() - self.assertEqual(new_creds.access_token, BASIC_TOKEN) - - # Verify mock. - self._verify_basic_refresh(http) - - def test_get_and_put_ndb(self): - # Start empty - storage = appengine.StorageByKeyName( - appengine.CredentialsNDBModel, 'foo', 'credentials') - self.assertEqual(None, storage.get()) - - # Refresh storage and retrieve without using storage - self.credentials.set_store(storage) - http = http_mock.HttpMock(data=BASIC_RESP) - self.credentials._refresh(http) - credmodel = appengine.CredentialsNDBModel.get_by_id('foo') - self.assertEqual(BASIC_TOKEN, credmodel.credentials.access_token) - self.assertEqual(credmodel.credentials.to_json(), - self.credentials.to_json()) - - # Verify mock. - self._verify_basic_refresh(http) - - def test_delete_ndb(self): - # Start empty - storage = appengine.StorageByKeyName( - appengine.CredentialsNDBModel, 'foo', 'credentials') - self.assertEqual(None, storage.get()) - - # Add credentials to model with storage, and check equivalent - # w/o storage - storage.put(self.credentials) - credmodel = appengine.CredentialsNDBModel.get_by_id('foo') - self.assertEqual(credmodel.credentials.to_json(), - self.credentials.to_json()) - - # Delete and make sure empty - storage.delete() - self.assertEqual(None, storage.get()) - - def test_get_and_put_mixed_ndb_storage_db_get(self): - # Start empty - storage = appengine.StorageByKeyName( - appengine.CredentialsNDBModel, 'foo', 'credentials') - self.assertEqual(None, storage.get()) - - # Set NDB store and refresh to add to storage - self.credentials.set_store(storage) - http = http_mock.HttpMock(data=BASIC_RESP) - self.credentials._refresh(http) - - # Retrieve same key from DB model to confirm mixing works - credmodel = appengine.CredentialsModel.get_by_key_name('foo') - self.assertEqual(BASIC_TOKEN, credmodel.credentials.access_token) - self.assertEqual(self.credentials.to_json(), - credmodel.credentials.to_json()) - - # Verify mock. - self._verify_basic_refresh(http) - - def test_get_and_put_mixed_db_storage_ndb_get(self): - # Start empty - storage = appengine.StorageByKeyName( - appengine.CredentialsModel, 'foo', 'credentials') - self.assertEqual(None, storage.get()) - - # Set DB store and refresh to add to storage - self.credentials.set_store(storage) - http = http_mock.HttpMock(data=BASIC_RESP) - self.credentials._refresh(http) - - # Retrieve same key from NDB model to confirm mixing works - credmodel = appengine.CredentialsNDBModel.get_by_id('foo') - self.assertEqual(BASIC_TOKEN, credmodel.credentials.access_token) - self.assertEqual(self.credentials.to_json(), - credmodel.credentials.to_json()) - - # Verify mock. - self._verify_basic_refresh(http) - - def test_delete_db_ndb_mixed(self): - # Start empty - storage_ndb = appengine.StorageByKeyName( - appengine.CredentialsNDBModel, 'foo', 'credentials') - storage = appengine.StorageByKeyName( - appengine.CredentialsModel, 'foo', 'credentials') - - # First DB, then NDB - self.assertEqual(None, storage.get()) - storage.put(self.credentials) - self.assertNotEqual(None, storage.get()) - - storage_ndb.delete() - self.assertEqual(None, storage.get()) - - # First NDB, then DB - self.assertEqual(None, storage_ndb.get()) - storage_ndb.put(self.credentials) - - storage.delete() - self.assertNotEqual(None, storage_ndb.get()) - # NDB uses memcache and an instance cache (Context) - ndb.get_context().clear_cache() - memcache.flush_all() - self.assertEqual(None, storage_ndb.get()) - - -class MockRequest(object): - url = 'https://example.org' - - def relative_url(self, rel): - return self.url + rel - - -class MockRequestHandler(object): - request = MockRequest() - - -class DecoratorTests(unittest.TestCase): - - def setUp(self): - self.testbed = testbed.Testbed() - self.testbed.activate() - self.testbed.init_datastore_v3_stub() - self.testbed.init_memcache_stub() - self.testbed.init_user_stub() - - decorator = appengine.OAuth2Decorator( - client_id='foo_client_id', client_secret='foo_client_secret', - scope=['foo_scope', 'bar_scope'], user_agent='foo') - - self._finish_setup(decorator, user_mock=UserMock) - - def _finish_setup(self, decorator, user_mock): - self.decorator = decorator - self.had_credentials = False - self.found_credentials = None - self.should_raise = False - parent = self - - class TestRequiredHandler(webapp2.RequestHandler): - @decorator.oauth_required - def get(self): - parent.assertTrue(decorator.has_credentials()) - parent.had_credentials = True - parent.found_credentials = decorator.credentials - if parent.should_raise: - raise parent.should_raise - - class TestAwareHandler(webapp2.RequestHandler): - @decorator.oauth_aware - def get(self, *args, **kwargs): - self.response.out.write('Hello World!') - assert(kwargs['year'] == '2012') - assert(kwargs['month'] == '01') - if decorator.has_credentials(): - parent.had_credentials = True - parent.found_credentials = decorator.credentials - if parent.should_raise: - raise parent.should_raise - - routes = [ - ('/oauth2callback', self.decorator.callback_handler()), - ('/foo_path', TestRequiredHandler), - webapp2.Route(r'/bar_path//', - handler=TestAwareHandler, name='bar'), - ] - application = webapp2.WSGIApplication(routes, debug=True) - - self.app = TestApp(application, extra_environ={ - 'wsgi.url_scheme': 'http', - 'HTTP_HOST': 'localhost', - }) - self.current_user = user_mock() - users.get_current_user = self.current_user - - def tearDown(self): - self.testbed.deactivate() - - def test_in_error(self): - # NOTE: This branch is never reached. _in_error is not set by any code - # path. It appears to be intended to be set during construction. - self.decorator._in_error = True - self.decorator._message = 'foobar' - - response = self.app.get('http://localhost/foo_path') - self.assertIn('foobar', response.body) - - response = self.app.get('http://localhost/bar_path/1234/56') - self.assertIn('foobar', response.body) - - def test_callback_application(self): - app = self.decorator.callback_application() - self.assertEqual( - app.router.match_routes[0].handler.__name__, - 'OAuth2Handler') - - @mock.patch('oauth2client.transport.get_http_object') - def test_required(self, new_http): - new_http.return_value = http_mock.HttpMock(data=DEFAULT_RESP) - # An initial request to an oauth_required decorated path should be a - # redirect to start the OAuth dance. - self.assertEqual(self.decorator.flow, None) - self.assertEqual(self.decorator.credentials, None) - response = self.app.get('http://localhost/foo_path') - self.assertTrue(response.status.startswith('302')) - q = urllib.parse.parse_qs( - response.headers['Location'].split('?', 1)[1]) - self.assertEqual('http://localhost/oauth2callback', - q['redirect_uri'][0]) - self.assertEqual('foo_client_id', q['client_id'][0]) - self.assertEqual('foo_scope bar_scope', q['scope'][0]) - self.assertEqual('http://localhost/foo_path', - q['state'][0].rsplit(':', 1)[0]) - self.assertEqual('code', q['response_type'][0]) - self.assertEqual(False, self.decorator.has_credentials()) - - with mock.patch.object(appengine, '_parse_state_value', - return_value='foo_path', - autospec=True) as parse_state_value: - # Now simulate the callback to /oauth2callback. - response = self.app.get('/oauth2callback', { - 'code': 'foo_access_code', - 'state': 'foo_path:xsrfkey123', - }) - parts = response.headers['Location'].split('?', 1) - self.assertEqual('http://localhost/foo_path', parts[0]) - self.assertEqual(None, self.decorator.credentials) - if self.decorator._token_response_param: - response_query = urllib.parse.parse_qs(parts[1]) - response = response_query[ - self.decorator._token_response_param][0] - self.assertEqual(json.loads(DEFAULT_RESP), - json.loads(urllib.parse.unquote(response))) - self.assertEqual(self.decorator.flow, self.decorator._tls.flow) - self.assertEqual(self.decorator.credentials, - self.decorator._tls.credentials) - - parse_state_value.assert_called_once_with( - 'foo_path:xsrfkey123', self.current_user) - - # Now requesting the decorated path should work. - response = self.app.get('/foo_path') - self.assertEqual('200 OK', response.status) - self.assertEqual(True, self.had_credentials) - self.assertEqual('foo_refresh_token', - self.found_credentials.refresh_token) - self.assertEqual('foo_access_token', - self.found_credentials.access_token) - self.assertEqual(None, self.decorator.credentials) - - # Raising an exception still clears the Credentials. - self.should_raise = Exception('') - with self.assertRaises(Exception): - self.app.get('/foo_path') - self.should_raise = False - self.assertEqual(None, self.decorator.credentials) - - # Access token refresh error should start the dance again - self.should_raise = client.AccessTokenRefreshError() - response = self.app.get('/foo_path') - self.should_raise = False - self.assertTrue(response.status.startswith('302')) - query_params = urllib.parse.parse_qs( - response.headers['Location'].split('?', 1)[1]) - self.assertEqual('http://localhost/oauth2callback', - query_params['redirect_uri'][0]) - - # Invalidate the stored Credentials. - self.found_credentials.invalid = True - self.found_credentials.store.put(self.found_credentials) - - # Invalid Credentials should start the OAuth dance again. - response = self.app.get('/foo_path') - self.assertTrue(response.status.startswith('302')) - query_params = urllib.parse.parse_qs( - response.headers['Location'].split('?', 1)[1]) - self.assertEqual('http://localhost/oauth2callback', - query_params['redirect_uri'][0]) - - # Check the mocks were called. - new_http.assert_called_once_with() - - @mock.patch('oauth2client.transport.get_http_object') - def test_storage_delete(self, new_http): - new_http.return_value = http_mock.HttpMock(data=DEFAULT_RESP) - # An initial request to an oauth_required decorated path should be a - # redirect to start the OAuth dance. - response = self.app.get('/foo_path') - self.assertTrue(response.status.startswith('302')) - - with mock.patch.object(appengine, '_parse_state_value', - return_value='foo_path', - autospec=True) as parse_state_value: - # Now simulate the callback to /oauth2callback. - response = self.app.get('/oauth2callback', { - 'code': 'foo_access_code', - 'state': 'foo_path:xsrfkey123', - }) - self.assertEqual('http://localhost/foo_path', - response.headers['Location']) - self.assertEqual(None, self.decorator.credentials) - - # Now requesting the decorated path should work. - response = self.app.get('/foo_path') - - self.assertTrue(self.had_credentials) - - # Credentials should be cleared after each call. - self.assertEqual(None, self.decorator.credentials) - - # Invalidate the stored Credentials. - self.found_credentials.store.delete() - - # Invalid Credentials should start the OAuth dance again. - response = self.app.get('/foo_path') - self.assertTrue(response.status.startswith('302')) - - parse_state_value.assert_called_once_with( - 'foo_path:xsrfkey123', self.current_user) - - # Check the mocks were called. - new_http.assert_called_once_with() - - @mock.patch('oauth2client.transport.get_http_object') - def test_aware(self, new_http): - new_http.return_value = http_mock.HttpMock(data=DEFAULT_RESP) - # An initial request to an oauth_aware decorated path should - # not redirect. - response = self.app.get('http://localhost/bar_path/2012/01') - self.assertEqual('Hello World!', response.body) - self.assertEqual('200 OK', response.status) - self.assertEqual(False, self.decorator.has_credentials()) - url = self.decorator.authorize_url() - q = urllib.parse.parse_qs(url.split('?', 1)[1]) - self.assertEqual('http://localhost/oauth2callback', - q['redirect_uri'][0]) - self.assertEqual('foo_client_id', q['client_id'][0]) - self.assertEqual('foo_scope bar_scope', q['scope'][0]) - self.assertEqual('http://localhost/bar_path/2012/01', - q['state'][0].rsplit(':', 1)[0]) - self.assertEqual('code', q['response_type'][0]) - - with mock.patch.object(appengine, '_parse_state_value', - return_value='bar_path', - autospec=True) as parse_state_value: - # Now simulate the callback to /oauth2callback. - url = self.decorator.authorize_url() - response = self.app.get('/oauth2callback', { - 'code': 'foo_access_code', - 'state': 'bar_path:xsrfkey456', - }) - - self.assertEqual('http://localhost/bar_path', - response.headers['Location']) - self.assertEqual(False, self.decorator.has_credentials()) - parse_state_value.assert_called_once_with( - 'bar_path:xsrfkey456', self.current_user) - - # Now requesting the decorated path will have credentials. - response = self.app.get('/bar_path/2012/01') - self.assertEqual('200 OK', response.status) - self.assertEqual('Hello World!', response.body) - self.assertEqual(True, self.had_credentials) - self.assertEqual('foo_refresh_token', - self.found_credentials.refresh_token) - self.assertEqual('foo_access_token', - self.found_credentials.access_token) - - # Credentials should be cleared after each call. - self.assertEqual(None, self.decorator.credentials) - - # Raising an exception still clears the Credentials. - self.should_raise = Exception('') - with self.assertRaises(Exception): - self.app.get('/bar_path/2012/01') - self.should_raise = False - self.assertEqual(None, self.decorator.credentials) - - # Check the mocks were called. - new_http.assert_called_once_with() - - def test_error_in_step2(self): - # An initial request to an oauth_aware decorated path should - # not redirect. - response = self.app.get('/bar_path/2012/01') - self.decorator.authorize_url() - response = self.app.get('/oauth2callback', { - 'error': 'BadHappened\'' - }) - self.assertEqual('200 OK', response.status) - self.assertTrue('Bad<Stuff>Happened'' in response.body) - - def test_kwargs_are_passed_to_underlying_flow(self): - decorator = appengine.OAuth2Decorator( - client_id='foo_client_id', client_secret='foo_client_secret', - user_agent='foo_user_agent', scope=['foo_scope', 'bar_scope'], - access_type='offline', prompt='consent', - revoke_uri='dummy_revoke_uri') - request_handler = MockRequestHandler() - decorator._create_flow(request_handler) - - self.assertEqual('https://example.org/oauth2callback', - decorator.flow.redirect_uri) - self.assertEqual('offline', decorator.flow.params['access_type']) - self.assertEqual('consent', decorator.flow.params['prompt']) - self.assertEqual('foo_user_agent', decorator.flow.user_agent) - self.assertEqual('dummy_revoke_uri', decorator.flow.revoke_uri) - self.assertEqual(None, decorator.flow.params.get('user_agent', None)) - self.assertEqual(decorator.flow, decorator._tls.flow) - - def test_token_response_param(self): - # No need to set-up a mock since test_required() does. - self.decorator._token_response_param = 'foobar' - self.test_required() - - @mock.patch('oauth2client.transport.get_http_object') - def test_decorator_from_client_secrets(self, new_http): - new_http.return_value = http_mock.HttpMock(data=DEFAULT_RESP) - # Execute test after setting up mock. - decorator = appengine.OAuth2DecoratorFromClientSecrets( - datafile('client_secrets.json'), - scope=['foo_scope', 'bar_scope']) - self._finish_setup(decorator, user_mock=UserMock) - - self.assertFalse(decorator._in_error) - self.decorator = decorator - self.test_required() - http = self.decorator.http() - self.assertEquals('foo_access_token', - http.request.credentials.access_token) - - # revoke_uri is not required - self.assertEqual(self.decorator._revoke_uri, - 'https://oauth2.googleapis.com/revoke') - self.assertEqual(self.decorator._revoke_uri, - self.decorator.credentials.revoke_uri) - - # Check the mocks were called. - new_http.assert_called_once_with() - - def test_decorator_from_client_secrets_toplevel(self): - decorator_patch = mock.patch( - 'oauth2client.contrib.appengine.OAuth2DecoratorFromClientSecrets') - - with decorator_patch as decorator_mock: - filename = datafile('client_secrets.json') - appengine.oauth2decorator_from_clientsecrets( - filename, scope='foo_scope') - decorator_mock.assert_called_once_with( - filename, - 'foo_scope', - cache=None, - message=None) - - def test_decorator_from_client_secrets_bad_type(self): - # NOTE: this code path is not currently reachable, as the only types - # that oauth2client.clientsecrets can load is web and installed, so - # this test forces execution of this code path. Despite not being - # normally reachable, this should remain in case future types of - # credentials are added. - - loadfile_patch = mock.patch( - 'oauth2client.contrib.appengine.clientsecrets.loadfile') - with loadfile_patch as loadfile_mock: - loadfile_mock.return_value = ('badtype', None) - with self.assertRaises(clientsecrets.InvalidClientSecretsError): - appengine.OAuth2DecoratorFromClientSecrets( - 'doesntmatter.json', - scope=['foo_scope', 'bar_scope']) - - def test_decorator_from_client_secrets_kwargs(self): - decorator = appengine.OAuth2DecoratorFromClientSecrets( - datafile('client_secrets.json'), - scope=['foo_scope', 'bar_scope'], - prompt='consent') - self.assertIn('prompt', decorator._kwargs) - - def test_decorator_from_cached_client_secrets(self): - cache_mock = http_mock.CacheMock() - load_and_cache('client_secrets.json', 'secret', cache_mock) - decorator = appengine.OAuth2DecoratorFromClientSecrets( - # filename, scope, message=None, cache=None - 'secret', '', cache=cache_mock) - self.assertFalse(decorator._in_error) - - def test_decorator_from_client_secrets_not_logged_in_required(self): - decorator = appengine.OAuth2DecoratorFromClientSecrets( - datafile('client_secrets.json'), - scope=['foo_scope', 'bar_scope'], message='NotLoggedInMessage') - self.decorator = decorator - self._finish_setup(decorator, user_mock=UserNotLoggedInMock) - - self.assertFalse(decorator._in_error) - - # An initial request to an oauth_required decorated path should be a - # redirect to login. - response = self.app.get('/foo_path') - self.assertTrue(response.status.startswith('302')) - self.assertTrue('Login' in str(response)) - - def test_decorator_from_client_secrets_not_logged_in_aware(self): - decorator = appengine.OAuth2DecoratorFromClientSecrets( - datafile('client_secrets.json'), - scope=['foo_scope', 'bar_scope'], message='NotLoggedInMessage') - self.decorator = decorator - self._finish_setup(decorator, user_mock=UserNotLoggedInMock) - - # An initial request to an oauth_aware decorated path should be a - # redirect to login. - response = self.app.get('/bar_path/2012/03') - self.assertTrue(response.status.startswith('302')) - self.assertTrue('Login' in str(response)) - - def test_decorator_from_unfilled_client_secrets_required(self): - MESSAGE = 'File is missing' - try: - appengine.OAuth2DecoratorFromClientSecrets( - datafile('unfilled_client_secrets.json'), - scope=['foo_scope', 'bar_scope'], message=MESSAGE) - except clientsecrets.InvalidClientSecretsError: - pass - - def test_decorator_from_unfilled_client_secrets_aware(self): - MESSAGE = 'File is missing' - try: - appengine.OAuth2DecoratorFromClientSecrets( - datafile('unfilled_client_secrets.json'), - scope=['foo_scope', 'bar_scope'], message=MESSAGE) - except clientsecrets.InvalidClientSecretsError: - pass - - def test_decorator_from_client_secrets_with_optional_settings(self): - # Test that the decorator works with the absense of a revoke_uri in - # the client secrets. - loadfile_patch = mock.patch( - 'oauth2client.contrib.appengine.clientsecrets.loadfile') - with loadfile_patch as loadfile_mock: - loadfile_mock.return_value = (clientsecrets.TYPE_WEB, { - 'client_id': 'foo_client_id', - 'client_secret': 'foo_client_secret', - 'redirect_uris': [], - 'auth_uri': oauth2client.GOOGLE_AUTH_URI, - 'token_uri': oauth2client.GOOGLE_TOKEN_URI, - # No revoke URI - }) - - decorator = appengine.OAuth2DecoratorFromClientSecrets( - 'doesntmatter.json', - scope=['foo_scope', 'bar_scope']) - - self.assertEqual(decorator._revoke_uri, oauth2client.GOOGLE_REVOKE_URI) - # This is never set, but it's consistent with other tests. - self.assertFalse(decorator._in_error) - - @mock.patch('oauth2client.transport.get_http_object') - def test_invalid_state(self, new_http): - new_http.return_value = http_mock.HttpMock(data=DEFAULT_RESP) - # Execute test after setting up mock. - with mock.patch.object(appengine, '_parse_state_value', - return_value=None, autospec=True): - # Now simulate the callback to /oauth2callback. - response = self.app.get('/oauth2callback', { - 'code': 'foo_access_code', - 'state': 'foo_path:xsrfkey123', - }) - self.assertEqual('200 OK', response.status) - self.assertEqual('The authorization request failed', response.body) - - # Check the mocks were called. - new_http.assert_called_once_with() - - -class DecoratorXsrfSecretTests(unittest.TestCase): - """Test xsrf_secret_key.""" - - def setUp(self): - self.testbed = testbed.Testbed() - self.testbed.activate() - self.testbed.init_datastore_v3_stub() - self.testbed.init_memcache_stub() - - def tearDown(self): - self.testbed.deactivate() - - def test_build_and_parse_state(self): - secret = appengine.xsrf_secret_key() - - # Secret shouldn't change from call to call. - secret2 = appengine.xsrf_secret_key() - self.assertEqual(secret, secret2) - - # Secret shouldn't change if memcache goes away. - memcache.delete(appengine.XSRF_MEMCACHE_ID, - namespace=appengine.OAUTH2CLIENT_NAMESPACE) - secret3 = appengine.xsrf_secret_key() - self.assertEqual(secret2, secret3) - - # Secret should change if both memcache and the model goes away. - memcache.delete(appengine.XSRF_MEMCACHE_ID, - namespace=appengine.OAUTH2CLIENT_NAMESPACE) - model = appengine.SiteXsrfSecretKey.get_or_insert('site') - model.delete() - - secret4 = appengine.xsrf_secret_key() - self.assertNotEqual(secret3, secret4) - - def test_ndb_insert_db_get(self): - secret = appengine._generate_new_xsrf_secret_key() - appengine.SiteXsrfSecretKeyNDB(id='site', secret=secret).put() - - site_key = appengine.SiteXsrfSecretKey.get_by_key_name('site') - self.assertEqual(site_key.secret, secret) - - def test_db_insert_ndb_get(self): - secret = appengine._generate_new_xsrf_secret_key() - appengine.SiteXsrfSecretKey(key_name='site', secret=secret).put() - - site_key = appengine.SiteXsrfSecretKeyNDB.get_by_id('site') - self.assertEqual(site_key.secret, secret) - - -class DecoratorXsrfProtectionTests(unittest.TestCase): - """Test _build_state_value and _parse_state_value.""" - - def setUp(self): - self.testbed = testbed.Testbed() - self.testbed.activate() - self.testbed.init_datastore_v3_stub() - self.testbed.init_memcache_stub() - - def tearDown(self): - self.testbed.deactivate() - - def test_build_and_parse_state(self): - state = appengine._build_state_value(MockRequestHandler(), UserMock()) - self.assertEqual( - 'https://example.org', - appengine._parse_state_value(state, UserMock())) - redirect_uri = appengine._parse_state_value(state[1:], UserMock()) - self.assertIsNone(redirect_uri) diff --git a/tests/contrib/django_util/test_decorators.py b/tests/contrib/django_util/test_decorators.py index f237f88..846c6dd 100644 --- a/tests/contrib/django_util/test_decorators.py +++ b/tests/contrib/django_util/test_decorators.py @@ -18,18 +18,18 @@ import copy from django import http import django.conf -from django.contrib.auth import models as django_models +from django.contrib.auth.models import AnonymousUser, User import mock from six.moves import http_client from six.moves import reload_module from six.moves.urllib import parse +from tests.contrib.django_util import TestWithDjangoEnvironment import oauth2client.contrib.django_util from oauth2client.contrib.django_util import decorators -from tests.contrib import django_util as tests_django_util -class OAuth2EnabledDecoratorTest(tests_django_util.TestWithDjangoEnvironment): +class OAuth2EnabledDecoratorTest(TestWithDjangoEnvironment): def setUp(self): super(OAuth2EnabledDecoratorTest, self).setUp() @@ -39,7 +39,7 @@ class OAuth2EnabledDecoratorTest(tests_django_util.TestWithDjangoEnvironment): # at import time, so in order for us to reload the settings # we need to reload the module reload_module(oauth2client.contrib.django_util) - self.user = django_models.User.objects.create_user( + self.user = User.objects.create_user( username='bill', email='bill@example.com', password='hunter2') def tearDown(self): @@ -63,7 +63,7 @@ class OAuth2EnabledDecoratorTest(tests_django_util.TestWithDjangoEnvironment): @mock.patch('oauth2client.client.OAuth2Credentials') def test_has_credentials_in_storage(self, OAuth2Credentials): request = self.factory.get('/test') - request.session = mock.Mock() + request.session = mock.MagicMock() credentials_mock = mock.Mock( scopes=set(django.conf.settings.GOOGLE_OAUTH2_SCOPES)) @@ -88,11 +88,11 @@ class OAuth2EnabledDecoratorTest(tests_django_util.TestWithDjangoEnvironment): @mock.patch('oauth2client.contrib.dictionary_storage.DictionaryStorage') def test_specified_scopes(self, dictionary_storage_mock): request = self.factory.get('/test') - request.session = mock.Mock() + request.session = mock.MagicMock() credentials_mock = mock.Mock( scopes=set(django.conf.settings.GOOGLE_OAUTH2_SCOPES)) - credentials_mock.has_scopes = mock.Mock(return_value=True) + credentials_mock.has_scopes = True credentials_mock.is_valid = True dictionary_storage_mock.get.return_value = credentials_mock @@ -106,14 +106,14 @@ class OAuth2EnabledDecoratorTest(tests_django_util.TestWithDjangoEnvironment): self.assertFalse(request.oauth.has_credentials()) -class OAuth2RequiredDecoratorTest(tests_django_util.TestWithDjangoEnvironment): +class OAuth2RequiredDecoratorTest(TestWithDjangoEnvironment): def setUp(self): super(OAuth2RequiredDecoratorTest, self).setUp() self.save_settings = copy.deepcopy(django.conf.settings) reload_module(oauth2client.contrib.django_util) - self.user = django_models.User.objects.create_user( + self.user = User.objects.create_user( username='bill', email='bill@example.com', password='hunter2') def tearDown(self): @@ -141,13 +141,13 @@ class OAuth2RequiredDecoratorTest(tests_django_util.TestWithDjangoEnvironment): @mock.patch('oauth2client.contrib.django_util.UserOAuth2', autospec=True) def test_has_credentials_in_storage(self, UserOAuth2): request = self.factory.get('/test') - request.session = mock.Mock() + request.session = mock.MagicMock() @decorators.oauth_required def test_view(request): return http.HttpResponse("test") - my_user_oauth = mock.Mock() + my_user_oauth = mock.MagicMock() UserOAuth2.return_value = my_user_oauth my_user_oauth.has_credentials.return_value = True @@ -161,7 +161,7 @@ class OAuth2RequiredDecoratorTest(tests_django_util.TestWithDjangoEnvironment): self, OAuth2Credentials): request = self.factory.get('/test') - request.session = mock.Mock() + request.session = mock.MagicMock() credentials_mock = mock.Mock( scopes=set(django.conf.settings.GOOGLE_OAUTH2_SCOPES)) credentials_mock.has_scopes.return_value = False @@ -179,11 +179,11 @@ class OAuth2RequiredDecoratorTest(tests_django_util.TestWithDjangoEnvironment): @mock.patch('oauth2client.client.OAuth2Credentials') def test_specified_scopes(self, OAuth2Credentials): request = self.factory.get('/test') - request.session = mock.Mock() + request.session = mock.MagicMock() credentials_mock = mock.Mock( scopes=set(django.conf.settings.GOOGLE_OAUTH2_SCOPES)) - credentials_mock.has_scopes = mock.Mock(return_value=False) + credentials_mock.has_scopes = False OAuth2Credentials.from_json.return_value = credentials_mock @decorators.oauth_required(scopes=['additional-scope']) @@ -195,8 +195,7 @@ class OAuth2RequiredDecoratorTest(tests_django_util.TestWithDjangoEnvironment): response.status_code, django.http.HttpResponseRedirect.status_code) -class OAuth2RequiredDecoratorStorageModelTest( - tests_django_util.TestWithDjangoEnvironment): +class OAuth2RequiredDecoratorStorageModelTest(TestWithDjangoEnvironment): def setUp(self): super(OAuth2RequiredDecoratorStorageModelTest, self).setUp() @@ -210,7 +209,7 @@ class OAuth2RequiredDecoratorStorageModelTest( django.conf.settings.GOOGLE_OAUTH2_STORAGE_MODEL = STORAGE_MODEL reload_module(oauth2client.contrib.django_util) - self.user = django_models.User.objects.create_user( + self.user = User.objects.create_user( username='bill', email='bill@example.com', password='hunter2') def tearDown(self): @@ -220,7 +219,7 @@ class OAuth2RequiredDecoratorStorageModelTest( def test_redirects_anonymous_to_login(self): request = self.factory.get('/test') request.session = self.session - request.user = django_models.AnonymousUser() + request.user = AnonymousUser() @decorators.oauth_required def test_view(request): @@ -234,7 +233,7 @@ class OAuth2RequiredDecoratorStorageModelTest( def test_redirects_user_to_oauth_authorize(self): request = self.factory.get('/test') request.session = self.session - request.user = django_models.User.objects.create_user( + request.user = User.objects.create_user( username='bill3', email='bill@example.com', password='hunter2') @decorators.oauth_required diff --git a/tests/contrib/django_util/test_django_models.py b/tests/contrib/django_util/test_django_models.py index da54965..aeaed15 100644 --- a/tests/contrib/django_util/test_django_models.py +++ b/tests/contrib/django_util/test_django_models.py @@ -19,42 +19,36 @@ Unit tests for models and fields defined by the django_util helper. import base64 import pickle -import unittest -import jsonpickle +from tests.contrib.django_util.models import CredentialsModel -from oauth2client import _helpers -from oauth2client import client -from oauth2client.contrib.django_util import models -from tests.contrib.django_util import models as tests_models +import unittest2 +from oauth2client._helpers import _from_bytes +from oauth2client.client import Credentials +from oauth2client.contrib.django_util.models import CredentialsField -class TestCredentialsField(unittest.TestCase): + +class TestCredentialsField(unittest2.TestCase): def setUp(self): - self.fake_model = tests_models.CredentialsModel() + self.fake_model = CredentialsModel() self.fake_model_field = self.fake_model._meta.get_field('credentials') - self.field = models.CredentialsField(null=True) - self.credentials = client.Credentials() - self.pickle_str = _helpers._from_bytes( + self.field = CredentialsField(null=True) + self.credentials = Credentials() + self.pickle_str = _from_bytes( base64.b64encode(pickle.dumps(self.credentials))) - self.jsonpickle_str = _helpers._from_bytes( - base64.b64encode(jsonpickle.encode(self.credentials).encode())) def test_field_is_text(self): self.assertEqual(self.field.get_internal_type(), 'BinaryField') def test_field_unpickled(self): self.assertIsInstance( - self.field.to_python(self.pickle_str), client.Credentials) - - def test_field_jsonunpickled(self): - self.assertIsInstance( - self.field.to_python(self.jsonpickle_str), client.Credentials) + self.field.to_python(self.pickle_str), Credentials) def test_field_already_unpickled(self): self.assertIsInstance( - self.field.to_python(self.credentials), client.Credentials) + self.field.to_python(self.credentials), Credentials) def test_none_field_unpickled(self): self.assertIsNone(self.field.to_python(None)) @@ -62,7 +56,7 @@ class TestCredentialsField(unittest.TestCase): def test_from_db_value(self): value = self.field.from_db_value( self.pickle_str, None, None, None) - self.assertIsInstance(value, client.Credentials) + self.assertIsInstance(value, Credentials) def test_field_unpickled_none(self): self.assertEqual(self.field.to_python(None), None) @@ -70,12 +64,12 @@ class TestCredentialsField(unittest.TestCase): def test_field_pickled(self): prep_value = self.field.get_db_prep_value(self.credentials, connection=None) - self.assertEqual(prep_value, self.jsonpickle_str) + self.assertEqual(prep_value, self.pickle_str) def test_field_value_to_string(self): self.fake_model.credentials = self.credentials value_str = self.fake_model_field.value_to_string(self.fake_model) - self.assertEqual(value_str, self.jsonpickle_str) + self.assertEqual(value_str, self.pickle_str) def test_field_value_to_string_none(self): self.fake_model.credentials = None @@ -83,11 +77,11 @@ class TestCredentialsField(unittest.TestCase): self.assertIsNone(value_str) def test_credentials_without_null(self): - credentials = models.CredentialsField() + credentials = CredentialsField() self.assertTrue(credentials.null) -class CredentialWithSetStore(models.CredentialsField): +class CredentialWithSetStore(CredentialsField): def __init__(self): self.model = CredentialWithSetStore @@ -102,4 +96,4 @@ class FakeCredentialsModelMock(object): class FakeCredentialsModelMockNoSet(object): - credentials = models.CredentialsField() + credentials = CredentialsField() diff --git a/tests/contrib/django_util/test_django_storage.py b/tests/contrib/django_util/test_django_storage.py index a608c94..8f76b18 100644 --- a/tests/contrib/django_util/test_django_storage.py +++ b/tests/contrib/django_util/test_django_storage.py @@ -16,10 +16,10 @@ # Mock a Django environment import datetime -import unittest from django.db import models import mock +import unittest2 from oauth2client import GOOGLE_TOKEN_URI from oauth2client.client import OAuth2Credentials @@ -28,7 +28,7 @@ from oauth2client.contrib.django_util.storage import ( DjangoORMStorage as Storage) -class TestStorage(unittest.TestCase): +class TestStorage(unittest2.TestCase): def setUp(self): access_token = 'foo' client_id = 'some_client_id' diff --git a/tests/contrib/django_util/test_django_util.py b/tests/contrib/django_util/test_django_util.py index 82d7be7..84457cb 100644 --- a/tests/contrib/django_util/test_django_util.py +++ b/tests/contrib/django_util/test_django_util.py @@ -15,19 +15,20 @@ """Tests the initialization logic of django_util.""" import copy -import unittest import django.conf from django.conf.urls import include, url -from django.contrib.auth import models as django_models +from django.contrib.auth.models import AnonymousUser from django.core import exceptions import mock from six.moves import reload_module +from tests.contrib.django_util import TestWithDjangoEnvironment +import unittest2 from oauth2client.contrib import django_util import oauth2client.contrib.django_util -from oauth2client.contrib.django_util import site -from tests.contrib import django_util as tests_django_util +from oauth2client.contrib.django_util import ( + _CREDENTIALS_KEY, get_storage, site, UserOAuth2) urlpatterns = [ @@ -35,7 +36,7 @@ urlpatterns = [ ] -class OAuth2SetupTest(unittest.TestCase): +class OAuth2SetupTest(unittest2.TestCase): def setUp(self): self.save_settings = copy.deepcopy(django.conf.settings) @@ -100,20 +101,6 @@ class OAuth2SetupTest(unittest.TestCase): object.__new__(django_util.OAuth2Settings), django.conf.settings) - def test_no_middleware(self): - django.conf.settings.MIDDLEWARE_CLASSES = None - with self.assertRaises(exceptions.ImproperlyConfigured): - django_util.OAuth2Settings.__init__( - object.__new__(django_util.OAuth2Settings), - django.conf.settings) - - def test_middleware_no_classes(self): - django.conf.settings.MIDDLEWARE = ( - django.conf.settings.MIDDLEWARE_CLASSES) - django.conf.settings.MIDDLEWARE_CLASSES = None - # primarily testing this doesn't raise an exception - django_util.OAuth2Settings(django.conf.settings) - def test_storage_model(self): STORAGE_MODEL = { 'model': 'tests.contrib.django_util.models.CredentialsModel', @@ -134,7 +121,7 @@ class MockObjectWithSession(object): self.session = session -class SessionStorageTest(tests_django_util.TestWithDjangoEnvironment): +class SessionStorageTest(TestWithDjangoEnvironment): def setUp(self): super(SessionStorageTest, self).setUp() @@ -146,19 +133,19 @@ class SessionStorageTest(tests_django_util.TestWithDjangoEnvironment): django.conf.settings = copy.deepcopy(self.save_settings) def test_session_delete(self): - self.session[django_util._CREDENTIALS_KEY] = "test_val" + self.session[_CREDENTIALS_KEY] = "test_val" request = MockObjectWithSession(self.session) - django_storage = django_util.get_storage(request) + django_storage = get_storage(request) django_storage.delete() - self.assertIsNone(self.session.get(django_util._CREDENTIALS_KEY)) + self.assertIsNone(self.session.get(_CREDENTIALS_KEY)) def test_session_delete_nothing(self): request = MockObjectWithSession(self.session) - django_storage = django_util.get_storage(request) + django_storage = get_storage(request) django_storage.delete() -class TestUserOAuth2Object(tests_django_util.TestWithDjangoEnvironment): +class TestUserOAuth2Object(TestWithDjangoEnvironment): def setUp(self): super(TestUserOAuth2Object, self).setUp() @@ -180,6 +167,6 @@ class TestUserOAuth2Object(tests_django_util.TestWithDjangoEnvironment): request = self.factory.get('oauth2/oauth2authorize', data={'return_url': '/return_endpoint'}) request.session = self.session - request.user = django_models.AnonymousUser() - oauth2 = django_util.UserOAuth2(request) + request.user = AnonymousUser() + oauth2 = UserOAuth2(request) self.assertIsNone(oauth2.credentials) diff --git a/tests/contrib/django_util/test_views.py b/tests/contrib/django_util/test_views.py index 0b3fe30..df0d11c 100644 --- a/tests/contrib/django_util/test_views.py +++ b/tests/contrib/django_util/test_views.py @@ -20,25 +20,27 @@ import json import django from django import http import django.conf -from django.contrib.auth import models as django_models +from django.contrib.auth.models import AnonymousUser, User import mock from six.moves import reload_module -from oauth2client import client +from tests.contrib.django_util import TestWithDjangoEnvironment +from tests.contrib.django_util.models import CredentialsModel + +from oauth2client.client import FlowExchangeError, OAuth2WebServerFlow import oauth2client.contrib.django_util from oauth2client.contrib.django_util import views -from tests.contrib import django_util as tests_django_util -from tests.contrib.django_util import models as tests_models +from oauth2client.contrib.django_util.models import CredentialsField -class OAuth2AuthorizeTest(tests_django_util.TestWithDjangoEnvironment): +class OAuth2AuthorizeTest(TestWithDjangoEnvironment): def setUp(self): super(OAuth2AuthorizeTest, self).setUp() self.save_settings = copy.deepcopy(django.conf.settings) reload_module(oauth2client.contrib.django_util) - self.user = django_models.User.objects.create_user( - username='bill', email='bill@example.com', password='hunter2') + self.user = User.objects.create_user( + username='bill', email='bill@example.com', password='hunter2') def tearDown(self): django.conf.settings = copy.deepcopy(self.save_settings) @@ -53,7 +55,7 @@ class OAuth2AuthorizeTest(tests_django_util.TestWithDjangoEnvironment): def test_authorize_anonymous_user(self): request = self.factory.get('oauth2/oauth2authorize') request.session = self.session - request.user = django_models.AnonymousUser() + request.user = AnonymousUser() response = views.oauth2_authorize(request) self.assertIsInstance(response, http.HttpResponseRedirect) @@ -66,8 +68,7 @@ class OAuth2AuthorizeTest(tests_django_util.TestWithDjangoEnvironment): self.assertIsInstance(response, http.HttpResponseRedirect) -class Oauth2AuthorizeStorageModelTest( - tests_django_util.TestWithDjangoEnvironment): +class Oauth2AuthorizeStorageModelTest(TestWithDjangoEnvironment): def setUp(self): super(Oauth2AuthorizeStorageModelTest, self).setUp() @@ -84,7 +85,7 @@ class Oauth2AuthorizeStorageModelTest( # at import time, so in order for us to reload the settings # we need to reload the module reload_module(oauth2client.contrib.django_util) - self.user = django_models.User.objects.create_user( + self.user = User.objects.create_user( username='bill', email='bill@example.com', password='hunter2') def tearDown(self): @@ -102,7 +103,7 @@ class Oauth2AuthorizeStorageModelTest( def test_authorize_anonymous_user_redirects_login(self): request = self.factory.get('oauth2/oauth2authorize') request.session = self.session - request.user = django_models.AnonymousUser() + request.user = AnonymousUser() response = views.oauth2_authorize(request) self.assertIsInstance(response, http.HttpResponseRedirect) # redirects to Django login @@ -116,53 +117,25 @@ class Oauth2AuthorizeStorageModelTest( response = views.oauth2_authorize(request) self.assertIsInstance(response, http.HttpResponseRedirect) - def test_authorized_user_no_credentials_redirects(self): - request = self.factory.get('oauth2/oauth2authorize', - data={'return_url': '/return_endpoint'}) - request.session = self.session - - authorized_user = django_models.User.objects.create_user( - username='bill2', email='bill@example.com', password='hunter2') - - tests_models.CredentialsModel.objects.create( - user_id=authorized_user, - credentials=None) - - request.user = authorized_user - response = views.oauth2_authorize(request) - self.assertIsInstance(response, http.HttpResponseRedirect) - - def test_already_authorized(self): + def test_authorized_user_not_logged_in_redirects(self): request = self.factory.get('oauth2/oauth2authorize', data={'return_url': '/return_endpoint'}) request.session = self.session - authorized_user = django_models.User.objects.create_user( + authorized_user = User.objects.create_user( username='bill2', email='bill@example.com', password='hunter2') + credentials = CredentialsField() - credentials = _Credentials() - tests_models.CredentialsModel.objects.create( + CredentialsModel.objects.create( user_id=authorized_user, credentials=credentials) request.user = authorized_user response = views.oauth2_authorize(request) self.assertIsInstance(response, http.HttpResponseRedirect) - self.assertEqual(response.url, '/return_endpoint') - - -class _Credentials(object): - # Can't use mock when testing Django models - # https://code.djangoproject.com/ticket/25493 - def __init__(self): - self.invalid = False - self.scopes = set() - - def has_scopes(self, _): - return True -class Oauth2CallbackTest(tests_django_util.TestWithDjangoEnvironment): +class Oauth2CallbackTest(TestWithDjangoEnvironment): def setUp(self): super(Oauth2CallbackTest, self).setUp() @@ -176,11 +149,11 @@ class Oauth2CallbackTest(tests_django_util.TestWithDjangoEnvironment): 'return_url': self.RETURN_URL, 'scopes': django.conf.settings.GOOGLE_OAUTH2_SCOPES } - self.user = django_models.User.objects.create_user( + self.user = User.objects.create_user( username='bill', email='bill@example.com', password='hunter2') - @mock.patch('oauth2client.contrib.django_util.views.jsonpickle') - def test_callback_works(self, jsonpickle_mock): + @mock.patch('oauth2client.contrib.django_util.views.pickle') + def test_callback_works(self, pickle): request = self.factory.get('oauth2/oauth2callback', data={ 'state': json.dumps(self.fake_state), 'code': 123 @@ -188,7 +161,7 @@ class Oauth2CallbackTest(tests_django_util.TestWithDjangoEnvironment): self.session['google_oauth2_csrf_token'] = self.CSRF_TOKEN - flow = client.OAuth2WebServerFlow( + flow = OAuth2WebServerFlow( client_id='clientid', client_secret='clientsecret', scope=['email'], @@ -196,10 +169,9 @@ class Oauth2CallbackTest(tests_django_util.TestWithDjangoEnvironment): redirect_uri=request.build_absolute_uri("oauth2/oauth2callback")) name = 'google_oauth2_flow_{0}'.format(self.CSRF_TOKEN) - pickled_flow = object() - self.session[name] = pickled_flow + self.session[name] = pickle.dumps(flow) flow.step2_exchange = mock.Mock() - jsonpickle_mock.decode.return_value = flow + pickle.loads.return_value = flow request.session = self.session request.user = self.user @@ -208,10 +180,9 @@ class Oauth2CallbackTest(tests_django_util.TestWithDjangoEnvironment): self.assertEqual( response.status_code, django.http.HttpResponseRedirect.status_code) self.assertEqual(response['Location'], self.RETURN_URL) - jsonpickle_mock.decode.assert_called_once_with(pickled_flow) - @mock.patch('oauth2client.contrib.django_util.views.jsonpickle') - def test_callback_handles_bad_flow_exchange(self, jsonpickle_mock): + @mock.patch('oauth2client.contrib.django_util.views.pickle') + def test_callback_handles_bad_flow_exchange(self, pickle): request = self.factory.get('oauth2/oauth2callback', data={ "state": json.dumps(self.fake_state), "code": 123 @@ -219,27 +190,25 @@ class Oauth2CallbackTest(tests_django_util.TestWithDjangoEnvironment): self.session['google_oauth2_csrf_token'] = self.CSRF_TOKEN - flow = client.OAuth2WebServerFlow( + flow = OAuth2WebServerFlow( client_id='clientid', client_secret='clientsecret', scope=['email'], state=json.dumps(self.fake_state), redirect_uri=request.build_absolute_uri('oauth2/oauth2callback')) - session_key = 'google_oauth2_flow_{0}'.format(self.CSRF_TOKEN) - pickled_flow = object() - self.session[session_key] = pickled_flow + self.session['google_oauth2_flow_{0}'.format(self.CSRF_TOKEN)] \ + = pickle.dumps(flow) def local_throws(code): - raise client.FlowExchangeError('test') + raise FlowExchangeError('test') flow.step2_exchange = local_throws - jsonpickle_mock.decode.return_value = flow + pickle.loads.return_value = flow request.session = self.session response = views.oauth2_callback(request) self.assertIsInstance(response, http.HttpResponseBadRequest) - jsonpickle_mock.decode.assert_called_once_with(pickled_flow) def test_error_returns_bad_request(self): request = self.factory.get('oauth2/oauth2callback', data={ @@ -249,15 +218,6 @@ class Oauth2CallbackTest(tests_django_util.TestWithDjangoEnvironment): self.assertIsInstance(response, http.HttpResponseBadRequest) self.assertIn(b'Authorization failed', response.content) - def test_error_escapes_html(self): - request = self.factory.get('oauth2/oauth2callback', data={ - 'error': '', - }) - response = views.oauth2_callback(request) - self.assertIsInstance(response, http.HttpResponseBadRequest) - self.assertNotIn(b'