From 022f688a7cfcfc40425fc03570cc2fbef2bc81b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Nov=C3=BD?= Date: Sat, 24 Aug 2019 19:21:24 +0200 Subject: [PATCH] Retire swauth Change-Id: Ib8e22a1e2e35d22a754943e34501305a0cfdd9b9 Depends-On: https://review.opendev.org/678368 See: http://lists.openstack.org/pipermail/openstack-discuss/2019-August/008416.html --- .coveragerc | 6 - .gitignore | 6 - .mailmap | 12 - .unittests | 4 - AUTHORS | 56 - CHANGELOG | 105 - CONTRIBUTING.rst | 22 - LICENSE | 202 - README.md | 87 - README.rst | 10 + babel.cfg | 2 - bin/swauth-add-account | 72 - bin/swauth-add-user | 110 - bin/swauth-cleanup-tokens | 169 - bin/swauth-delete-account | 63 - bin/swauth-delete-user | 63 - bin/swauth-list | 86 - bin/swauth-prep | 62 - bin/swauth-set-account-service | 74 - bindep.txt | 5 - doc/build/.gitignore | 1 - .../Documentation LICENSE.txt | 9 - ...ty Explained, Rodney Beede, 2013-12-13.pdf | Bin 65665 -> 0 bytes ...Auth snippet, Rodney Beede, 2013-12-13.xml | 120 - .../swift_swauth_roles_matrix.png | Bin 37329 -> 0 bytes doc/source/_static/.empty | 0 doc/source/_templates/.empty | 0 doc/source/api.rst | 468 -- doc/source/authtypes.rst | 10 - doc/source/conf.py | 234 - doc/source/details.rst | 159 - doc/source/index.rst | 188 - doc/source/license.rst | 225 - doc/source/middleware.rst | 9 - doc/source/swauth.rst | 9 - etc/proxy-server.conf-sample | 86 - requirements.txt | 7 - setup.cfg | 63 - setup.py | 29 - swauth/__init__.py | 34 - swauth/authtypes.py | 238 - swauth/middleware.py | 1709 ------- swauth/swift_version.py | 82 - test-requirements.txt | 13 - test/__init__.py | 6 - test/unit/__init__.py | 0 test/unit/test_authtypes.py | 207 - test/unit/test_middleware.py | 4197 ----------------- test/unit/test_swift_version.py | 184 - tox.ini | 55 - webadmin/index.html | 575 --- 51 files changed, 10 insertions(+), 10123 deletions(-) delete mode 100644 .coveragerc delete mode 100644 .gitignore delete mode 100644 .mailmap delete mode 100755 .unittests delete mode 100644 AUTHORS delete mode 100644 CHANGELOG delete mode 100644 CONTRIBUTING.rst delete mode 100644 LICENSE delete mode 100644 README.md create mode 100644 README.rst delete mode 100644 babel.cfg delete mode 100755 bin/swauth-add-account delete mode 100755 bin/swauth-add-user delete mode 100755 bin/swauth-cleanup-tokens delete mode 100755 bin/swauth-delete-account delete mode 100755 bin/swauth-delete-user delete mode 100755 bin/swauth-list delete mode 100755 bin/swauth-prep delete mode 100755 bin/swauth-set-account-service delete mode 100644 bindep.txt delete mode 100644 doc/build/.gitignore delete mode 100644 doc/source/Draft Security Guide/Documentation LICENSE.txt delete mode 100644 doc/source/Draft Security Guide/SWAuth Security Explained, Rodney Beede, 2013-12-13.pdf delete mode 100644 doc/source/Draft Security Guide/docbook SWAuth snippet, Rodney Beede, 2013-12-13.xml delete mode 100644 doc/source/Draft Security Guide/swift_swauth_roles_matrix.png delete mode 100644 doc/source/_static/.empty delete mode 100644 doc/source/_templates/.empty delete mode 100644 doc/source/api.rst delete mode 100644 doc/source/authtypes.rst delete mode 100644 doc/source/conf.py delete mode 100644 doc/source/details.rst delete mode 100644 doc/source/index.rst delete mode 100644 doc/source/license.rst delete mode 100644 doc/source/middleware.rst delete mode 100644 doc/source/swauth.rst delete mode 100644 etc/proxy-server.conf-sample delete mode 100644 requirements.txt delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 swauth/__init__.py delete mode 100644 swauth/authtypes.py delete mode 100644 swauth/middleware.py delete mode 100644 swauth/swift_version.py delete mode 100644 test-requirements.txt delete mode 100644 test/__init__.py delete mode 100644 test/unit/__init__.py delete mode 100644 test/unit/test_authtypes.py delete mode 100644 test/unit/test_middleware.py delete mode 100644 test/unit/test_swift_version.py delete mode 100644 tox.ini delete mode 100644 webadmin/index.html diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 52c7ed7..0000000 --- a/.coveragerc +++ /dev/null @@ -1,6 +0,0 @@ -[run] -branch = True -omit = /usr*,setup.py,*egg*,.venv/*,.tox/*,test/* - -[report] -ignore_errors = True diff --git a/.gitignore b/.gitignore deleted file mode 100644 index f6c38b3..0000000 --- a/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -*.egg-info -*.py[co] -.DS_Store -.coverage -.tox -cover diff --git a/.mailmap b/.mailmap deleted file mode 100644 index 35232e2..0000000 --- a/.mailmap +++ /dev/null @@ -1,12 +0,0 @@ -Greg Holt -Greg Holt -Greg Holt -Greg Holt -Greg Holt -Greg Holt -Greg Holt -Greg Holt -Christian Schwede -Christian Schwede -Ondřej Nový -Ron Pedde diff --git a/.unittests b/.unittests deleted file mode 100755 index ccc1339..0000000 --- a/.unittests +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -nosetests test/unit --exe --with-coverage --cover-package swauth --cover-erase -rm -f .coverage diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index 5fd7db7..0000000 --- a/AUTHORS +++ /dev/null @@ -1,56 +0,0 @@ -Maintainers ------------ -Ondřej Nový -Peter Lisák - -Original Authors ----------------- -Chuck Thier -Greg Holt -Greg Lange -Jay Payne -John Dickinson -Michael Barton -Will Reese - -Contributors ------------- -Abdul Nizamuddin -Andreas Jaeger -Andrew Clay Shafer -Anne Gentle -Apollon Oikonomopoulos -Brian Cline -Brian K. Jones -Caleb Tennis -Chmouel Boudjnah -Chris Wedgwood -Christian Schwede -Christopher Bartz -Clay Gerrard -Colin Nicholson -Conrad Weidenkeller -Cory Wright -David Goetz -Ed Leafe -Eohyung Lee -Fujita Tomonori -jgrmnprz -Kapil Thangavelu -Marcelo Martins -Monty Taylor -Pablo Llopis -Paul Jimenez -Pawnesh Kumar -Pete Zaitcev -Prashanth Pai -Rodney Beede -Ron Pedde -Russ Nelson -Scott Simpson -SoftDed -Soren Hansen -Stephen Milton -Thiago da Silva -Tim Burke -Zhang Guoqing diff --git a/CHANGELOG b/CHANGELOG deleted file mode 100644 index 2eb2468..0000000 --- a/CHANGELOG +++ /dev/null @@ -1,105 +0,0 @@ -swauth (1.3.0) - - [SECURITY] Stop using client headers for cross-middleware communication - WARNING: You need to upgrade Swift3 to at least 1.12 - - [SECURITY] Hash token before storing it in Swift (CVE-2017-16613) - WARNING: In deployments without memcached this patch logs out all users - because tokens became invalid. - -swauth (1.2.0) - - Allow to set password by hash - - Allow to set hash salt in config for S3 compatibility - - Due to security reason, S3 support is disabled by default - - Salt is not included in S3 HMAC computation - - Use correct content type on JSON responses - - Fix changing of auth_type in existing deployments - - Remove outdated locale - -swauth (1.1.0) - - This is first release after move to OpenStack Infra - - Allow users to change their own password/key - - Add support for storage policy - - Show password prompt if key is not specified - - Allow to use Keystone at same time - - Support SHA512 for password hashing - - Code cleanup - - Bugfixies a security fixies - -swauth (1.0.8) - - Added request.environ[reseller_request] = True if request is coming from an - user in .reseller_admin group - - Fixed to work with newer Swift versions whose memcache clients require a - time keyword argument when the older versions required a timeout keyword - argument. - -swauth (1.0.7) - - New X-Auth-Token-Lifetime header a user can set to how long they'd like - their token to be good for. - - New max_token_life config value for capping the above. - - New X-Auth-Token-Expires header returned with the get token request. - - Switchover to swift.common.swob instead of WebOb; requires Swift >= 1.7.6 - now. - -swauth (1.0.6) - - Apparently I haven't been keeping up with this CHANGELOG. I'll try to be - better onward. - - This release added passing OPTIONS requests through untouched, needed for - CORS support in Swift. - - Also, Swauth is a bit more restrictive in deciding when it's the definitive - auth for a request. - -swauth (1.0.3-dev) - - This release is still under development. A full change log will be made at - release. Until then, you can see what has changed with: - - git log 1.0.2..HEAD - -swauth (1.0.2) - - Fixed bug rejecting requests when using multiple instances of Swauth or - Swauth with other auth services. - - Fixed bug interpreting URL-encoded user names and keys. - - Added support for the Swift container sync feature. - - Allowed /not/ setting super_admin_key to disable Swauth administration - features. - - Added swauth_remote mode so the Swauth middleware for one Swift cluster - could be pointing to the Swauth service on another Swift cluster, sharing - account/user data sets. - - Added ability to purge stored tokens. - - Added API documentation for internal Swauth API. - -swauth (1.0.1) - - Initial release after separation from Swift. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index 6cce2f4..0000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,22 +0,0 @@ -If you would like to contribute to the development of OpenStack, you must -follow the steps in this page: - - https://docs.openstack.org/infra/manual/developers.html - -If you already have a good understanding of how the system works and your -OpenStack accounts are set up, you can skip to the development workflow -section of this documentation to learn how changes to OpenStack should be -submitted for review via the Gerrit tool: - - https://docs.openstack.org/infra/manual/developers.html#development-workflow - -Please don't feel offended by difference of opinion. Be prepared to advocate -for your change and iterate on it based on feedback. Reach out to other people -working on the project in #openstack-swauth on freenode -[IRC](http://eavesdrop.openstack.org/irclogs/%23openstack-swauth/) - we want to help. - -Pull requests submitted through GitHub will be ignored. - -Bugs should be filed on Launchpad, not GitHub: - - https://bugs.launchpad.net/swauth diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 75b5248..0000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 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. diff --git a/README.md b/README.md deleted file mode 100644 index c08f62d..0000000 --- a/README.md +++ /dev/null @@ -1,87 +0,0 @@ -Swauth ------- - -An Auth Service for Swift as WSGI Middleware that uses Swift itself as a -backing store. Docs at: or ask in #openstack-swauth on -freenode [IRC](http://eavesdrop.openstack.org/irclogs/%23openstack-swauth/). - -See also for the standard OpenStack -auth service. - - -NOTE ----- - -**Be sure to review the docs at: -** - - -Quick Install -------------- - -1) Install Swauth with ``sudo python setup.py install`` or ``sudo python - setup.py develop`` or via whatever packaging system you may be using. - -2) Alter your proxy-server.conf pipeline to have swauth instead of tempauth: - - Was: - - [pipeline:main] - pipeline = catch_errors cache tempauth proxy-server - - Change To: - - [pipeline:main] - pipeline = catch_errors cache swauth proxy-server - -3) Add to your proxy-server.conf the section for the Swauth WSGI filter: - - [filter:swauth] - use = egg:swauth#swauth - set log_name = swauth - super_admin_key = swauthkey - -4) Be sure your proxy server allows account management: - - [app:proxy-server] - ... - allow_account_management = true - -5) Restart your proxy server ``swift-init proxy reload`` - -6) Initialize the Swauth backing store in Swift ``swauth-prep -K swauthkey`` - -7) Add an account/user ``swauth-add-user -A http://127.0.0.1:8080/auth/ -K - swauthkey -a test tester testing`` - -8) Ensure it works ``swift -A http://127.0.0.1:8080/auth/v1.0 -U test:tester -K - testing stat -v`` - - -Web Admin Install ------------------ - -1) If you installed from packages, you'll need to cd to the webadmin directory - the package installed. This is ``/usr/share/doc/python-swauth/webadmin`` - with the Lucid packages. If you installed from source, you'll need to cd to - the webadmin directory in the source directory. - -2) Upload the Web Admin files with ``swift -A http://127.0.0.1:8080/auth/v1.0 - -U .super_admin:.super_admin -K swauthkey upload .webadmin .`` - -3) Open ``http://127.0.0.1:8080/auth/`` in your browser. - - -Swift3 Middleware Compatibility -------------------------------- -[**Swift3 middleware**](https://github.com/openstack/swift3) can be used with -swauth when `auth_type` in swauth is configured to be *Plaintext* (default). - - [pipeline:main] - pipeline = catch_errors cache swift3 swauth proxy-server - -It can be used with `auth_type` set to Sha1/Sha512 too but with certain caveats -and security concern. Hence, s3 support is disabled by default and you have to -explicitly enable it in your configuration. -Refer to swift3 compatibility [section](https://swauth.readthedocs.io/en/latest/#swift3-middleware-compatibility) -in documentation for further details diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..86e34d6 --- /dev/null +++ b/README.rst @@ -0,0 +1,10 @@ +This project is no longer maintained. + +The contents of this repository are still available in the Git +source code management system. To see the contents of this +repository before it reached its end of life, please check out the +previous commit with "git checkout HEAD^1". + +For any further questions, please email +openstack-discuss@lists.openstack.org or join #openstack-dev on +Freenode. diff --git a/babel.cfg b/babel.cfg deleted file mode 100644 index 15cd6cb..0000000 --- a/babel.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[python: **.py] - diff --git a/bin/swauth-add-account b/bin/swauth-add-account deleted file mode 100755 index f33e491..0000000 --- a/bin/swauth-add-account +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2010-2011 OpenStack, LLC. -# -# 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 getpass import getpass -import gettext -from optparse import OptionParser -from sys import argv -from sys import exit - -from six.moves.urllib.parse import urlparse -from swift.common.bufferedhttp import http_connect_raw as http_connect - - -if __name__ == '__main__': - gettext.install('swauth', unicode=1) - parser = OptionParser(usage='Usage: %prog [options] ') - parser.add_option('-s', '--suffix', dest='suffix', - default='', help='The suffix to use with the reseller prefix as the ' - 'storage account name (default: ) Note: If ' - 'the account already exists, this will have no effect on existing ' - 'service URLs. Those will need to be updated with ' - 'swauth-set-account-service') - parser.add_option('-A', '--admin-url', dest='admin_url', - default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' - 'subsystem (default: http://127.0.0.1:8080/auth/)') - parser.add_option('-U', '--admin-user', dest='admin_user', - default='.super_admin', help='The user with admin rights to add users ' - '(default: .super_admin).') - parser.add_option('-K', '--admin-key', dest='admin_key', - help='The key for the user with admin rights to add users.') - args = argv[1:] - if not args: - args.append('-h') - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.parse_args(['-h']) - if not options.admin_key: - options.admin_key = getpass() - account = args[0] - parsed = urlparse(options.admin_url) - if parsed.scheme not in ('http', 'https'): - raise ValueError('Cannot handle protocol scheme %s for url %s' % - (parsed.scheme, repr(options.admin_url))) - parsed_path = parsed.path - if not parsed_path: - parsed_path = '/' - elif parsed_path[-1] != '/': - parsed_path += '/' - path = '%sv2/%s' % (parsed_path, account) - headers = {'X-Auth-Admin-User': options.admin_user, - 'X-Auth-Admin-Key': options.admin_key, - 'Content-Length': '0'} - if options.suffix: - headers['X-Account-Suffix'] = options.suffix - conn = http_connect(parsed.hostname, parsed.port, 'PUT', path, headers, - ssl=(parsed.scheme == 'https')) - resp = conn.getresponse() - if resp.status // 100 != 2: - exit('Account creation failed: %s %s' % (resp.status, resp.reason)) diff --git a/bin/swauth-add-user b/bin/swauth-add-user deleted file mode 100755 index f0c79be..0000000 --- a/bin/swauth-add-user +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2010-2011 OpenStack, LLC. -# -# 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 getpass import getpass -import gettext -from optparse import OptionParser -from sys import argv -from sys import exit - -from six.moves.urllib.parse import urlparse -from swift.common.bufferedhttp import http_connect_raw as http_connect - - -if __name__ == '__main__': - gettext.install('swauth', unicode=1) - parser = OptionParser( - usage='Usage: %prog [options] ') - parser.add_option('-a', '--admin', dest='admin', action='store_true', - default=False, help='Give the user administrator access; otherwise ' - 'the user will only have access to containers specifically allowed ' - 'with ACLs.') - parser.add_option('-r', '--reseller-admin', dest='reseller_admin', - action='store_true', default=False, help='Give the user full reseller ' - 'administrator access, giving them full access to all accounts within ' - 'the reseller, including the ability to create new accounts. Creating ' - 'a new reseller admin requires super_admin rights.') - parser.add_option('-s', '--suffix', dest='suffix', - default='', help='The suffix to use with the reseller prefix as the ' - 'storage account name (default: ) Note: If ' - 'the account already exists, this will have no effect on existing ' - 'service URLs. Those will need to be updated with ' - 'swauth-set-account-service') - parser.add_option('-e', '--hashed', dest='password_hashed', - action='store_true', default=False, help='Supplied password is ' - 'already hashed and in format :') - parser.add_option('-A', '--admin-url', dest='admin_url', - default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' - 'subsystem (default: http://127.0.0.1:8080/auth/') - parser.add_option('-U', '--admin-user', dest='admin_user', - default='.super_admin', help='The user with admin rights to add users ' - '(default: .super_admin).') - parser.add_option('-K', '--admin-key', dest='admin_key', - help='The key for the user with admin rights to add users.') - args = argv[1:] - if not args: - args.append('-h') - (options, args) = parser.parse_args(args) - if len(args) != 3: - parser.parse_args(['-h']) - if not options.admin_key: - options.admin_key = getpass() - account, user, password = args - parsed = urlparse(options.admin_url) - if parsed.scheme not in ('http', 'https'): - raise ValueError('Cannot handle protocol scheme %s for url %s' % - (parsed.scheme, repr(options.admin_url))) - parsed_path = parsed.path - if not parsed_path: - parsed_path = '/' - elif parsed_path[-1] != '/': - parsed_path += '/' - # Ensure the account exists if user is NOT trying to change his password - if not options.admin_user == (account + ':' + user): - path = '%sv2/%s' % (parsed_path, account) - headers = {'X-Auth-Admin-User': options.admin_user, - 'X-Auth-Admin-Key': options.admin_key} - if options.suffix: - headers['X-Account-Suffix'] = options.suffix - conn = http_connect(parsed.hostname, parsed.port, 'GET', path, headers, - ssl=(parsed.scheme == 'https')) - resp = conn.getresponse() - if resp.status // 100 != 2: - headers['Content-Length'] = '0' - conn = http_connect(parsed.hostname, parsed.port, 'PUT', path, - headers, ssl=(parsed.scheme == 'https')) - resp = conn.getresponse() - if resp.status // 100 != 2: - print('Account creation failed: %s %s' % - (resp.status, resp.reason)) - # Add the user - path = '%sv2/%s/%s' % (parsed_path, account, user) - headers = {'X-Auth-Admin-User': options.admin_user, - 'X-Auth-Admin-Key': options.admin_key, - 'Content-Length': '0'} - if options.admin: - headers['X-Auth-User-Admin'] = 'true' - if options.reseller_admin: - headers['X-Auth-User-Reseller-Admin'] = 'true' - if options.password_hashed: - headers['X-Auth-User-Key-Hash'] = password - else: - headers['X-Auth-User-Key'] = password - conn = http_connect(parsed.hostname, parsed.port, 'PUT', path, headers, - ssl=(parsed.scheme == 'https')) - resp = conn.getresponse() - if resp.status // 100 != 2: - exit('User creation failed: %s %s' % (resp.status, resp.reason)) diff --git a/bin/swauth-cleanup-tokens b/bin/swauth-cleanup-tokens deleted file mode 100755 index 60e6bd8..0000000 --- a/bin/swauth-cleanup-tokens +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2010-2011 OpenStack, LLC. -# -# 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 datetime import datetime -from datetime import timedelta -from getpass import getpass -import gettext -import json -from optparse import OptionParser -import re -from sys import argv -from sys import exit -from time import sleep -from time import time - -from swiftclient.client import ClientException -from swiftclient.client import Connection - -if __name__ == '__main__': - gettext.install('swauth', unicode=1) - parser = OptionParser(usage='Usage: %prog [options]') - parser.add_option('-t', '--token-life', dest='token_life', - default='86400', help='The expected life of tokens; token objects ' - 'modified more than this number of seconds ago will be checked for ' - 'expiration (default: 86400).') - parser.add_option('-s', '--sleep', dest='sleep', - default='0.1', help='The number of seconds to sleep between token ' - 'checks (default: 0.1)') - parser.add_option('-v', '--verbose', dest='verbose', action='store_true', - default=False, help='Outputs everything done instead of just the ' - 'deletions.') - parser.add_option('-A', '--admin-url', dest='admin_url', - default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' - 'subsystem (default: http://127.0.0.1:8080/auth/)') - parser.add_option('-K', '--admin-key', dest='admin_key', - help='The key for .super_admin.') - parser.add_option('', '--purge', dest='purge_account', help='Purges all ' - 'tokens for a given account whether the tokens have expired or not.') - parser.add_option('', '--purge-all', dest='purge_all', action='store_true', - default=False, help='Purges all tokens for all accounts and users ' - 'whether the tokens have expired or not.') - args = argv[1:] - if not args: - args.append('-h') - (options, args) = parser.parse_args(args) - if len(args) != 0: - parser.parse_args(['-h']) - if not options.admin_key: - options.admin_key = getpass() - options.admin_url = options.admin_url.rstrip('/') - if not options.admin_url.endswith('/v1.0'): - options.admin_url += '/v1.0' - options.admin_user = '.super_admin:.super_admin' - options.token_life = timedelta(0, float(options.token_life)) - options.sleep = float(options.sleep) - conn = Connection(options.admin_url, options.admin_user, options.admin_key) - if options.purge_account: - marker = None - while True: - if options.verbose: - print('GET %s?marker=%s' % (options.purge_account, marker)) - objs = conn.get_container(options.purge_account, marker=marker)[1] - if objs: - marker = objs[-1]['name'] - else: - if options.verbose: - print('No more objects in %s' % options.purge_account) - break - for obj in objs: - if options.verbose: - print('HEAD %s/%s' % (options.purge_account, obj['name'])) - headers = conn.head_object(options.purge_account, obj['name']) - if 'x-object-meta-auth-token' in headers: - token = headers['x-object-meta-auth-token'] - container = '.token_%s' % token[-1] - if options.verbose: - print('%s/%s purge account %r; deleting' % - (container, token, options.purge_account)) - print('DELETE %s/%s' % (container, token)) - try: - conn.delete_object(container, token) - except ClientException as err: - if err.http_status != 404: - raise - continue - if options.verbose: - print('Done.') - exit(0) - for x in range(16): - container = '.token_%x' % x - marker = None - while True: - if options.verbose: - print('GET %s?marker=%s' % (container, marker)) - try: - objs = conn.get_container(container, marker=marker)[1] - except ClientException as e: - if e.http_status == 404: - exit('Container %s not found. swauth-prep needs to be ' - 'rerun' % (container)) - else: - exit('Object listing on container %s failed with status ' - 'code %d' % (container, e.http_status)) - if objs: - marker = objs[-1]['name'] - else: - if options.verbose: - print('No more objects in %s' % container) - break - for obj in objs: - if options.purge_all: - if options.verbose: - print('%s/%s purge all; deleting' % - (container, obj['name'])) - print('DELETE %s/%s' % (container, obj['name'])) - try: - conn.delete_object(container, obj['name']) - except ClientException as err: - if err.http_status != 404: - raise - continue - last_modified = datetime(*map(int, re.split(r'[^\d]', - obj['last_modified'])[:-1])) - ago = datetime.utcnow() - last_modified - if ago > options.token_life: - if options.verbose: - print('%s/%s last modified %ss ago; investigating' % - (container, obj['name'], - ago.days * 86400 + ago.seconds)) - print('GET %s/%s' % (container, obj['name'])) - detail = conn.get_object(container, obj['name'])[1] - detail = json.loads(detail) - if detail['expires'] < time(): - if options.verbose: - print('%s/%s expired %ds ago; deleting' % - (container, obj['name'], - time() - detail['expires'])) - print('DELETE %s/%s' % (container, obj['name'])) - try: - conn.delete_object(container, obj['name']) - except ClientException as e: - if e.http_status != 404: - print('DELETE of %s/%s failed with status ' - 'code %d' % (container, obj['name'], - e.http_status)) - elif options.verbose: - print("%s/%s won't expire for %ds; skipping" % - (container, obj['name'], - detail['expires'] - time())) - elif options.verbose: - print('%s/%s last modified %ss ago; skipping' % - (container, obj['name'], - ago.days * 86400 + ago.seconds)) - sleep(options.sleep) - if options.verbose: - print('Done.') diff --git a/bin/swauth-delete-account b/bin/swauth-delete-account deleted file mode 100755 index 724597d..0000000 --- a/bin/swauth-delete-account +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2010-2011 OpenStack, LLC. -# -# 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 getpass import getpass -import gettext -from optparse import OptionParser -from sys import argv -from sys import exit - -from six.moves.urllib.parse import urlparse -from swift.common.bufferedhttp import http_connect_raw as http_connect - - -if __name__ == '__main__': - gettext.install('swauth', unicode=1) - parser = OptionParser(usage='Usage: %prog [options] ') - parser.add_option('-A', '--admin-url', dest='admin_url', - default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' - 'subsystem (default: http://127.0.0.1:8080/auth/') - parser.add_option('-U', '--admin-user', dest='admin_user', - default='.super_admin', help='The user with admin rights to add users ' - '(default: .super_admin).') - parser.add_option('-K', '--admin-key', dest='admin_key', - help='The key for the user with admin rights to add users.') - args = argv[1:] - if not args: - args.append('-h') - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.parse_args(['-h']) - if not options.admin_key: - options.admin_key = getpass() - account = args[0] - parsed = urlparse(options.admin_url) - if parsed.scheme not in ('http', 'https'): - raise ValueError('Cannot handle protocol scheme %s for url %s' % - (parsed.scheme, repr(options.admin_url))) - parsed_path = parsed.path - if not parsed_path: - parsed_path = '/' - elif parsed_path[-1] != '/': - parsed_path += '/' - path = '%sv2/%s' % (parsed_path, account) - headers = {'X-Auth-Admin-User': options.admin_user, - 'X-Auth-Admin-Key': options.admin_key} - conn = http_connect(parsed.hostname, parsed.port, 'DELETE', path, headers, - ssl=(parsed.scheme == 'https')) - resp = conn.getresponse() - if resp.status // 100 != 2: - exit('Account deletion failed: %s %s' % (resp.status, resp.reason)) diff --git a/bin/swauth-delete-user b/bin/swauth-delete-user deleted file mode 100755 index c1cb209..0000000 --- a/bin/swauth-delete-user +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2010-2011 OpenStack, LLC. -# -# 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 getpass import getpass -import gettext -from optparse import OptionParser -from sys import argv -from sys import exit - -from six.moves.urllib.parse import urlparse -from swift.common.bufferedhttp import http_connect_raw as http_connect - - -if __name__ == '__main__': - gettext.install('swauth', unicode=1) - parser = OptionParser(usage='Usage: %prog [options] ') - parser.add_option('-A', '--admin-url', dest='admin_url', - default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' - 'subsystem (default: http://127.0.0.1:8080/auth/') - parser.add_option('-U', '--admin-user', dest='admin_user', - default='.super_admin', help='The user with admin rights to add users ' - '(default: .super_admin).') - parser.add_option('-K', '--admin-key', dest='admin_key', - help='The key for the user with admin rights to add users.') - args = argv[1:] - if not args: - args.append('-h') - (options, args) = parser.parse_args(args) - if len(args) != 2: - parser.parse_args(['-h']) - if not options.admin_key: - options.admin_key = getpass() - account, user = args - parsed = urlparse(options.admin_url) - if parsed.scheme not in ('http', 'https'): - raise ValueError('Cannot handle protocol scheme %s for url %s' % - (parsed.scheme, repr(options.admin_url))) - parsed_path = parsed.path - if not parsed_path: - parsed_path = '/' - elif parsed_path[-1] != '/': - parsed_path += '/' - path = '%sv2/%s/%s' % (parsed_path, account, user) - headers = {'X-Auth-Admin-User': options.admin_user, - 'X-Auth-Admin-Key': options.admin_key} - conn = http_connect(parsed.hostname, parsed.port, 'DELETE', path, headers, - ssl=(parsed.scheme == 'https')) - resp = conn.getresponse() - if resp.status // 100 != 2: - exit('User deletion failed: %s %s' % (resp.status, resp.reason)) diff --git a/bin/swauth-list b/bin/swauth-list deleted file mode 100755 index 53916e2..0000000 --- a/bin/swauth-list +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2010-2011 OpenStack, LLC. -# -# 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 getpass import getpass -import gettext -import json -from optparse import OptionParser -from sys import argv -from sys import exit - -from six.moves.urllib.parse import urlparse -from swift.common.bufferedhttp import http_connect_raw as http_connect - - -if __name__ == '__main__': - gettext.install('swauth', unicode=1) - parser = OptionParser(usage=''' -Usage: %prog [options] [account] [user] - -If [account] and [user] are omitted, a list of accounts will be output. - -If [account] is included but not [user], an account's information will be -output, including a list of users within the account. - -If [account] and [user] are included, the user's information will be output, -including a list of groups the user belongs to. - -If the [user] is '.groups', the active groups for the account will be listed. -'''.strip()) - parser.add_option('-p', '--plain-text', dest='plain_text', - action='store_true', default=False, help='Changes the output from ' - 'JSON to plain text. This will cause an account to list only the ' - 'users and a user to list only the groups.') - parser.add_option('-A', '--admin-url', dest='admin_url', - default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' - 'subsystem (default: http://127.0.0.1:8080/auth/') - parser.add_option('-U', '--admin-user', dest='admin_user', - default='.super_admin', help='The user with admin rights to add users ' - '(default: .super_admin).') - parser.add_option('-K', '--admin-key', dest='admin_key', - help='The key for the user with admin rights to add users.') - args = argv[1:] - if not args: - args.append('-h') - (options, args) = parser.parse_args(args) - if len(args) > 2: - parser.parse_args(['-h']) - if not options.admin_key: - options.admin_key = getpass() - parsed = urlparse(options.admin_url) - if parsed.scheme not in ('http', 'https'): - raise ValueError('Cannot handle protocol scheme %s for url %s' % - (parsed.scheme, repr(options.admin_url))) - parsed_path = parsed.path - if not parsed_path: - parsed_path = '/' - elif parsed_path[-1] != '/': - parsed_path += '/' - path = '%sv2/%s' % (parsed_path, '/'.join(args)) - headers = {'X-Auth-Admin-User': options.admin_user, - 'X-Auth-Admin-Key': options.admin_key} - conn = http_connect(parsed.hostname, parsed.port, 'GET', path, headers, - ssl=(parsed.scheme == 'https')) - resp = conn.getresponse() - body = resp.read() - if resp.status // 100 != 2: - exit('List failed: %s %s' % (resp.status, resp.reason)) - if options.plain_text: - info = json.loads(body) - for group in info[['accounts', 'users', 'groups'][len(args)]]: - print(group['name']) - else: - print(body) diff --git a/bin/swauth-prep b/bin/swauth-prep deleted file mode 100755 index 70fc1f0..0000000 --- a/bin/swauth-prep +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2010-2011 OpenStack, LLC. -# -# 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 getpass import getpass -import gettext -from optparse import OptionParser -from sys import argv -from sys import exit - -from six.moves.urllib.parse import urlparse -from swift.common.bufferedhttp import http_connect_raw as http_connect - - -if __name__ == '__main__': - gettext.install('swauth', unicode=1) - parser = OptionParser(usage='Usage: %prog [options]') - parser.add_option('-A', '--admin-url', dest='admin_url', - default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' - 'subsystem (default: http://127.0.0.1:8080/auth/') - parser.add_option('-U', '--admin-user', dest='admin_user', - default='.super_admin', help='The user with admin rights to add users ' - '(default: .super_admin).') - parser.add_option('-K', '--admin-key', dest='admin_key', - help='The key for the user with admin rights to add users.') - args = argv[1:] - if not args: - args.append('-h') - (options, args) = parser.parse_args(args) - if args: - parser.parse_args(['-h']) - if not options.admin_key: - options.admin_key = getpass() - parsed = urlparse(options.admin_url) - if parsed.scheme not in ('http', 'https'): - raise ValueError('Cannot handle protocol scheme %s for url %s' % - (parsed.scheme, repr(options.admin_url))) - parsed_path = parsed.path - if not parsed_path: - parsed_path = '/' - elif parsed_path[-1] != '/': - parsed_path += '/' - path = '%sv2/.prep' % parsed_path - headers = {'X-Auth-Admin-User': options.admin_user, - 'X-Auth-Admin-Key': options.admin_key} - conn = http_connect(parsed.hostname, parsed.port, 'POST', path, headers, - ssl=(parsed.scheme == 'https')) - resp = conn.getresponse() - if resp.status // 100 != 2: - exit('Auth subsystem prep failed: %s %s' % (resp.status, resp.reason)) diff --git a/bin/swauth-set-account-service b/bin/swauth-set-account-service deleted file mode 100755 index ff45e6f..0000000 --- a/bin/swauth-set-account-service +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2010-2011 OpenStack, LLC. -# -# 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 getpass import getpass -import gettext -import json -from optparse import OptionParser -from sys import argv -from sys import exit - -from six.moves.urllib.parse import urlparse -from swift.common.bufferedhttp import http_connect_raw as http_connect - - -if __name__ == '__main__': - gettext.install('swauth', unicode=1) - parser = OptionParser(usage=''' -Usage: %prog [options] - -Sets a service URL for an account. Can only be set by a reseller admin. - -Example: %prog -K swauthkey test storage local - http://127.0.0.1:8080/v1/AUTH_018c3946-23f8-4efb-a8fb-b67aae8e4162 -'''.strip()) - parser.add_option('-A', '--admin-url', dest='admin_url', - default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' - 'subsystem (default: http://127.0.0.1:8080/auth/)') - parser.add_option('-U', '--admin-user', dest='admin_user', - default='.super_admin', help='The user with admin rights to add users ' - '(default: .super_admin).') - parser.add_option('-K', '--admin-key', dest='admin_key', - help='The key for the user with admin rights to add users.') - args = argv[1:] - if not args: - args.append('-h') - (options, args) = parser.parse_args(args) - if len(args) != 4: - parser.parse_args(['-h']) - if not options.admin_key: - options.admin_key = getpass() - account, service, name, url = args - parsed = urlparse(options.admin_url) - if parsed.scheme not in ('http', 'https'): - raise ValueError('Cannot handle protocol scheme %s for url %s' % - (parsed.scheme, repr(options.admin_url))) - parsed_path = parsed.path - if not parsed_path: - parsed_path = '/' - elif parsed_path[-1] != '/': - parsed_path += '/' - path = '%sv2/%s/.services' % (parsed_path, account) - body = json.dumps({service: {name: url}}) - headers = {'Content-Length': str(len(body)), - 'X-Auth-Admin-User': options.admin_user, - 'X-Auth-Admin-Key': options.admin_key} - conn = http_connect(parsed.hostname, parsed.port, 'POST', path, headers, - ssl=(parsed.scheme == 'https')) - conn.send(body) - resp = conn.getresponse() - if resp.status // 100 != 2: - exit('Service set failed: %s %s' % (resp.status, resp.reason)) diff --git a/bindep.txt b/bindep.txt deleted file mode 100644 index 4f5e3c1..0000000 --- a/bindep.txt +++ /dev/null @@ -1,5 +0,0 @@ -# This is a cross-platform list tracking distribution packages needed by tests; -# see http://docs.openstack.org/infra/bindep/ for additional information. - -liberasurecode-dev [platform:dpkg] -liberasurecode-devel [platform:rpm] diff --git a/doc/build/.gitignore b/doc/build/.gitignore deleted file mode 100644 index 72e8ffc..0000000 --- a/doc/build/.gitignore +++ /dev/null @@ -1 +0,0 @@ -* diff --git a/doc/source/Draft Security Guide/Documentation LICENSE.txt b/doc/source/Draft Security Guide/Documentation LICENSE.txt deleted file mode 100644 index 242e8a3..0000000 --- a/doc/source/Draft Security Guide/Documentation LICENSE.txt +++ /dev/null @@ -1,9 +0,0 @@ -This document is licensed under Creative Commons Attribution 3.0 License. - - - -http://creativecommons.org/licenses/by/3.0/legalcode - - -Rodney Beede -http://www.rodneybeede.com/ \ No newline at end of file diff --git a/doc/source/Draft Security Guide/SWAuth Security Explained, Rodney Beede, 2013-12-13.pdf b/doc/source/Draft Security Guide/SWAuth Security Explained, Rodney Beede, 2013-12-13.pdf deleted file mode 100644 index 06114d1e799d06191baf49b401908c9874d8d5b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65665 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≶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 QLFoAqX=wv - SWAuth - SWAuth is another popular alternative to Keystone. - In contrast to Keystone it stores the user accounts, - credentials, and metadata in object storage itself. More - specifics about where the objects are stored can be found - on the SWAuth website at - http://gholt.github.io/swauth/. - SWAuth has these types of roles (or groups) for a - user: - - - - .super_admin - Can perform any action on any - OpenStack Account, Container, or Object - - - .reseller_admin - Can perform most actions on - any OpenStack Account, Container, or Object. - Cannot create other reseller admins. - - - - .admin - Can perform actions limited to - the single OpenStack Account it belongs to - - - Regular User - Can access containers or - objects they have permission to in the - OpenStack Account to which they belong - - - - - The following table provides a matrix of what each - role/group can do: -
- Object storage SWAuth role matrix - - - - - - - - -
- The super admin key is stored in - /etc/swift/proxy-server.conf and MUST - be protected! See the File Permissions section for - guidance on protecting this file. Frequent changing of - this key is recommended. - One approach for administration is to create an - OpenStack Object Storage Account called "CloudAdmins" and - create reseller_admin users in that account. Each user - will be able to do administrative functions in all the - other accounts. Creating a reseller_admin will require - the super admin key. - Another useful way to secure the super admin key is - to have it exist only on the proxy server and retrieve the - key on-demand via ssh or by running the command on the - proxy server itself and using a grep to extract the key on - the fly. -
- Protecting cloud administration - When using SWAuth you can actually designate - that certain proxy service nodes are to NOT allow - administrator API calls. This is useful if you have - Proxy service nodes on the public Internet and wish to - restrict administration functions to only special - Proxy service nodes on a private network. This is - done by setting the - allow_account_managment to false in your - proxy-server.conf. - Another important consideration is that the - SWAuth command line tools expose the user credentials - on the command-line. The system from which they are - executed must be secure to prevent disclosure in the - process list to other uses. Another option is to use - the SWAuth admin REST API to implement your own admin - CLI tools that don’t expose the key as a command-line - option. -
-
- Salting and hashing passwords - SWAuth by default stores passwords in - clear-text. It also offers a sha1 hashing provider, - but the salt used is global. Additionally, no - iterations or key stretching is performed. This is a - limitation of SWAuth. - You may optionally add-in your own hashing code - or provider as a hook to SWAuth. See the - SWAuth code and site - for details. - If you use the global salt be sure to secure it - and back it up. If you have multiple proxy nodes each - one has to have a copy so that may be good enough for - you. If you ever lose it or change it then all - existing user passwords will not work and will have to - be reset. - You should make sure the salt you choose is - generated using a cryptographically secure random - number generator and of sufficient length. At least - 20 characters is recommended. - The salt is stored in the - /etc/swift/proxy-server.conf - file which must be secured with proper ACLs. See the - File Permissions section for guidance. -
- - \ No newline at end of file diff --git a/doc/source/Draft Security Guide/swift_swauth_roles_matrix.png b/doc/source/Draft Security Guide/swift_swauth_roles_matrix.png deleted file mode 100644 index 9401c4a69dcb937756c70ad47c9bf2d5bc6b48d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37329 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

k3BE2L~e|k@s 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 Kc#)XC_x}Nal;ITs diff --git a/doc/source/_static/.empty b/doc/source/_static/.empty deleted file mode 100644 index e69de29..0000000 diff --git a/doc/source/_templates/.empty b/doc/source/_templates/.empty deleted file mode 100644 index e69de29..0000000 diff --git a/doc/source/api.rst b/doc/source/api.rst deleted file mode 100644 index e5effe4..0000000 --- a/doc/source/api.rst +++ /dev/null @@ -1,468 +0,0 @@ -.. _api_top: - ----------- -Swauth API ----------- - -Overview -======== - -Swauth has its own internal versioned REST API for adding, removing, -and editing accounts. This document explains the v2 API. - -Authentication --------------- - -Each REST request against the swauth API requires the inclusion of a -specific authorization user and key to be passed in a specific HTTP -header. These headers are defined as ``X-Auth-Admin-User`` and -``X-Auth-Admin-Key``. - -Typically, these values are ``.super_admin`` (the site super admin -user) with the key being specified in the swauth middleware -configuration as ``super_admin_key``. - -This could also be a reseller admin with the appropriate rights to -perform actions on reseller accounts. - -Endpoints ---------- - -The swauth API endpoint is presented on the proxy servers, in the -"/auth" namespace. In addition, the API is versioned, and the version -documented is version 2. API versions subdivide the auth namespace by -version, specified as a version identifier like "v2". - -The auth endpoint described herein is therefore located at "/auth/v2/" -as presented by the proxy servers. - -Bear in mind that in order for the auth management API to be -presented, it must be enabled in the proxy server config by setting -``allow_account_managment`` to ``true`` in the ``[app:proxy-server]`` -stanza of your proxy-server.conf. - -Responses ---------- - -Responses from the auth APIs are returned as a JSON structure. -Example return values in this document are edited for readability. - - -Reseller/Admin Services -======================= - -Operations can be performed against the endpoint itself to perform -general administrative operations. Currently, the only operations -that can be performed is a GET operation to get reseller or site admin -information. - -Get Admin Info --------------- - -A GET request at the swauth endpoint will return reseller information -for the account specified in the ``X-Auth-Admin-User`` header. -Currently, the information returned is limited to a list of accounts -for the reseller or site admin. - -Valid return codes: - * 200: Success - * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key - * 5xx: Internal error - -Example Request:: - - GET /auth// HTTP/1.1 - X-Auth-Admin-User: .super_admin - X-Auth-Admin-Key: swauthkey - -Example Curl Request:: - - curl -D - https:///auth/v2/ \ - -H "X-Auth-Admin-User: .super_admin" \ - -H "X-Auth-Admin-Key: swauthkey" - -Example Result:: - - HTTP/1.1 200 OK - - { "accounts": - [ - { "name": "account1" }, - { "name": "account2" }, - { "name": "account3" } - ] - } - - -Account Services -================ - -There are API request to get account details, create, and delete -accounts, mapping logically to the REST verbs GET, PUT, and DELETE. -These actions are performed against an account URI, in the following -general request structure:: - - METHOD /auth// HTTP/1.1 - -The methods that can be used are detailed below. - -Get Account Details -------------------- - -Account details can be retrieved by performing a GET request against -an account URI. On success, a JSON dictionary will be returned -containing the keys `account_id`, `services`, and `users`. The -`account_id` is the value used when creating service accounts. The -`services` value is a dict that represents valid storage cluster -endpoints, and which endpoint is the default. The 'users' value is a -list of dicts, each dict representing a user and currently only -containing the single key 'name'. - -Valid Responses: - * 200: Success - * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key - * 5xx: Internal error - -Example Request:: - - GET /auth// HTTP/1.1 - X-Auth-Admin-User: .super_admin - X-Auth-Admin-Key: swauthkey - -Example Curl Request:: - - curl -D - https:///auth/v2/ \ - -H "X-Auth-Admin-User: .super_admin" \ - -H "X-Auth-Admin-Key: swauthkey" - -Example Response:: - - HTTP/1.1 200 OK - - { "services": - { "storage": - { "default": "local", - "local": "https:///v1/" } - }, - "account_id": "", - "users": [ { "name": "user1" }, - { "name": "user2" } ] - } - -Create Account --------------- - -An account can be created with a PUT request against a non-existent -account. By default, a newly created UUID4 will be used with the -reseller prefix as the account ID used when creating corresponding -service accounts. However, you can provide an X-Account-Suffix header -to replace the UUDI4 part. - -Valid return codes: - * 200: Success - * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key - * 5xx: Internal error - -Example Request:: - - PUT /auth// HTTP/1.1 - X-Auth-Admin-User: .super_admin - X-Auth-Admin-Key: swauthkey - -Example Curl Request:: - - curl -XPUT -D - https:///auth/v2/ \ - -H "X-Auth-Admin-User: .super_admin" \ - -H "X-Auth-Admin-Key: swauthkey" - -Example Response:: - - HTTP/1.1 201 Created - - -Delete Account --------------- - -An account can be deleted with a DELETE request against an existing -account. - -Valid Responses: - * 204: Success - * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key - * 404: Account not found - * 5xx: Internal error - -Example Request:: - - DELETE /auth// HTTP/1.1 - X-Auth-Admin-User: .super_admin - X-Auth-Admin-Key: swauthkey - -Example Curl Request:: - - curl -XDELETE -D - https:///auth/v2/ \ - -H "X-Auth-Admin-User: .super_admin" \ - -H "X-Auth-Admin-Key: swauthkey" - -Example Response:: - - HTTP/1.1 204 No Content - - -User Services -============= - -Each account in swauth contains zero or more users. These users can -be determined with the 'Get Account Details' API request against an -account. - -Users in an account can be created, modified, and detailed as -described below by apply the appropriate REST verbs to a user URI, in -the following general request structure:: - - METHOD /auth/// HTTP/1.1 - -The methods that can be used are detailed below. - -Get User Details ----------------- - -User details can be retrieved by performing a GET request against -a user URI. On success, a JSON dictionary will be returned as -described:: - - {"groups": [ # List of groups the user is a member of - {"name": ":"}, - # The first group is a unique user identifier - {"name": ""}, - # The second group is the auth account name - {"name": ""} - # There may be additional groups, .admin being a - # special group indicating an account admin and - # .reseller_admin indicating a reseller admin. - ], - "auth": ":" - # The auth-type and key for the user; currently only - # plaintext and sha1 are implemented as auth types. - } - -For example:: - - {"groups": [{"name": "test:tester"}, {"name": "test"}, - {"name": ".admin"}], - "auth": "plaintext:testing"} - -Valid Responses: - * 200: Success - * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key - * 404: Unknown account - * 5xx: Internal error - -Example Request:: - - GET /auth/// HTTP/1.1 - X-Auth-Admin-User: .super_admin - X-Auth-Admin-Key: swauthkey - -Example Curl Request:: - - curl -D - https:///auth/v2// \ - -H "X-Auth-Admin-User: .super_admin" \ - -H "X-Auth-Admin-Key: swauthkey" - -Example Response:: - - HTTP/1.1 200 Ok - - { "groups": [ { "name": ":" }, - { "name": "" }, - { "name": ".admin" } ], - "auth" : "plaintext:password" } - - -Create User ------------ - -A user can be created with a PUT request against a non-existent -user URI. The new user's password must be set using the -``X-Auth-User-Key`` header. The user name MUST NOT start with a -period ('.'). This requirement is enforced by the API, and will -result in a 400 error. Alternatively you can use -``X-Auth-User-Key-Hash`` header for providing already hashed -password in format ``:``. - -Optional Headers: - - * ``X-Auth-User-Admin: true``: create the user as an account admin - * ``X-Auth-User-Reseller-Admin: true``: create the user as a reseller - admin - -Reseller admin accounts can only be created by the site admin, while -regular accounts (or account admin accounts) can be created by an -account admin, an appropriate reseller admin, or the site admin. - -Note that PUT requests are idempotent, and the PUT request serves as -both a request and modify action. - -Valid Responses: - * 200: Success - * 400: Invalid request (missing required headers) - * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key, or insufficient priv - * 404: Unknown account - * 5xx: Internal error - -Example Request:: - - PUT /auth/// HTTP/1.1 - X-Auth-Admin-User: .super_admin - X-Auth-Admin-Key: swauthkey - X-Auth-User-Admin: true - X-Auth-User-Key: secret - -Example Curl Request:: - - curl -XPUT -D - https:///auth/v2// \ - -H "X-Auth-Admin-User: .super_admin" \ - -H "X-Auth-Admin-Key: swauthkey" \ - -H "X-Auth-User-Admin: true" \ - -H "X-Auth-User-Key: secret" - -Example Response:: - - HTTP/1.1 201 Created - -Delete User ------------ - -A user can be deleted by performing a DELETE request against a user -URI. This action can only be performed by an account admin, -appropriate reseller admin, or site admin. - -Valid Responses: - * 200: Success - * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key, or insufficient priv - * 404: Unknown account or user - * 5xx: Internal error - -Example Request:: - - DELETE /auth/// HTTP/1.1 - X-Auth-Admin-User: .super_admin - X-Auth-Admin-Key: swauthkey - -Example Curl Request:: - - curl -XDELETE -D - https:///auth/v2// \ - -H "X-Auth-Admin-User: .super_admin" \ - -H "X-Auth-Admin-Key: swauthkey" - -Example Response:: - - HTTP/1.1 204 No Content - - -Other Services -============== - -There are several other swauth functions that can be performed, mostly -done via "pseudo-user" accounts. These are well-known user names that -are unable to be actually provisioned. These pseudo-users are -described below. - -.. _api_set_service_endpoints: - -Set Service Endpoints ---------------------- - -Service endpoint information can be retrived using the _`Get Account -Details` API method. - -This function allows setting values within this section for -the , allowing the addition of new service end points -or updating existing ones by performing a POST to the URI -corresponding to the pseudo-user ".services". - -The body of the POST request should contain a JSON dict with -the following format:: - - {"service_name": {"end_point_name": "end_point_value"}} - -There can be multiple services and multiple end points in the -same call. - -Any new services or end points will be added to the existing -set of services and end points. Any existing services with the -same service name will be merged with the new end points. Any -existing end points with the same end point name will have -their values updated. - -The updated services dictionary will be returned on success. - -Valid Responses: - - * 200: Success - * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key - * 404: Account not found - * 5xx: Internal error - -Example Request:: - - POST /auth///.services HTTP/1.0 - X-Auth-Admin-User: .super_admin - X-Auth-Admin-Key: swauthkey - - {"storage": { "local": "" }} - -Example Curl Request:: - - curl -XPOST -D - https:///auth/v2//.services \ - -H "X-Auth-Admin-User: .super_admin" \ - -H "X-Auth-Admin-Key: swauthkey" --data-binary \ - '{ "storage": { "local": "" }}' - -Example Response:: - - HTTP/1.1 200 OK - - {"storage": {"default": "local", "local": "" }} - -Get Account Groups ------------------- - -Individual user group information can be retrieved using the `Get User Details`_ API method. - -This function allows retrieving all group information for all users in -an existing account. This can be achieved using a GET action against -a user URI with the pseudo-user ".groups". - -The JSON dictionary returned will be a "groups" dictionary similar to -that documented in the `Get User Details`_ method, but representing -the summary of all groups utilized by all active users in the account. - -Valid Responses: - * 200: Success - * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key - * 404: Account not found - * 5xx: Internal error - -Example Request:: - - GET /auth///.groups - X-Auth-Admin-User: .super_admin - X-Auth-Admin-Key: swauthkey - -Example Curl Request:: - - curl -D - https:///auth/v2//.groups \ - -H "X-Auth-Admin-User: .super_admin" \ - -H "X-Auth-Admin-Key: swauthkey" - -Example Response:: - - HTTP/1.1 200 OK - - { "groups": [ { "name": ".admin" }, - { "name": "" }, - { "name": ":user1" }, - { "name": ":user2" } ] } - diff --git a/doc/source/authtypes.rst b/doc/source/authtypes.rst deleted file mode 100644 index a19ee22..0000000 --- a/doc/source/authtypes.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. _swauth_authtypes_module: - -swauth.authtypes -================= - -.. automodule:: swauth.authtypes - :members: - :undoc-members: - :show-inheritance: - :noindex: diff --git a/doc/source/conf.py b/doc/source/conf.py deleted file mode 100644 index 3c31ec1..0000000 --- a/doc/source/conf.py +++ /dev/null @@ -1,234 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2010-2011 OpenStack, LLC. -# -# 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. - -# -# Swauth documentation build configuration file, created by -# sphinx-quickstart on Mon Feb 14 19:34:51 2011. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys, os - -import swauth - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ----------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'Swauth' -copyright = u'2010-2011, OpenStack, LLC' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -from swauth import __version__ -version = __version__.rsplit('.', 1)[0] -# The full version, including alpha/beta/rc tags. -release = swauth.__version__ - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = [] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'Swauthdoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -# The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'Swauth.tex', u'Swauth Documentation', - u'OpenStack, LLC', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'swauth', u'Swauth Documentation', - [u'OpenStack, LLC'], 1) -] diff --git a/doc/source/details.rst b/doc/source/details.rst deleted file mode 100644 index 8dd057b..0000000 --- a/doc/source/details.rst +++ /dev/null @@ -1,159 +0,0 @@ ----------------------- -Implementation Details ----------------------- - -The Swauth system is a scalable authentication and authorization system that -uses Swift itself as its backing store. This section will describe how it -stores its data. - -.. note:: - - You can access Swauth's internal .auth account by using the account:user of - .super_admin:.super_admin and the super admin key you have set in your - configuration. Here's an example using `st` on a standard SAIO: ``st -A - http://127.0.0.1:8080/auth/v1.0 -U .super_admin:.super_admin -K swauthkey - stat`` - -At the topmost level, the auth system has its own Swift account it stores its -own account information within. This Swift account is known as -self.auth_account in the code and its name is in the format -self.reseller_prefix + ".auth". In this text, we'll refer to this account as -. - -The containers whose names do not begin with a period represent the accounts -within the auth service. For example, the /test container would -represent the "test" account. - -The objects within each container represent the users for that auth service -account. For example, the /test/bob object would represent the -user "bob" within the auth service account of "test". Each of these user -objects contain a JSON dictionary of the format:: - - {"auth": ":", "groups": } - -The `` specifies how the user key is encoded. The default is `plaintext`, -which saves the user's key in plaintext in the `` field. -The value `sha1` is supported as well, which stores the user's key as a salted -SHA1 hash. Note that using a one-way hash like SHA1 will likely inhibit future use of key-signing request types, assuming such support is added. The `` can be specified in the swauth section of the proxy server's -config file, along with the salt value in the following way:: - - auth_type = - auth_type_salt = - -Both fields are optional. auth_type defaults to `plaintext` and auth_type_salt defaults to "swauthsalt". Additional auth types can be implemented along with existing ones in the authtypes.py module. - -The `` contains at least two groups. The first is a unique group -identifying that user and it's name is of the format `:`. The -second group is the `` itself. Additional groups of `.admin` for -account administrators and `.reseller_admin` for reseller administrators may -exist. Here's an example user JSON dictionary:: - - {"auth": "plaintext:testing", - "groups": [{"name": "test:tester"}, {"name": "test"}, {"name": ".admin"}]} - -To map an auth service account to a Swift storage account, the Service Account -Id string is stored in the `X-Container-Meta-Account-Id` header for the -/ container. To map back the other way, an -/.account_id/ object is created with the contents of -the corresponding auth service's account name. - -Also, to support a future where the auth service will support multiple Swift -clusters or even multiple services for the same auth service account, an -//.services object is created with its contents having a -JSON dictionary of the format:: - - {"storage": {"default": "local", "local": }} - -The "default" is always "local" right now, and "local" is always the single -Swift cluster URL; but in the future there can be more than one cluster with -various names instead of just "local", and the "default" key's value will -contain the primary cluster to use for that account. Also, there may be more -services in addition to the current "storage" service right now. - -Here's an example .services dictionary at the moment:: - - {"storage": - {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9"}} - -But, here's an example of what the dictionary may look like in the future:: - - {"storage": - {"default": "dfw", - "dfw": "http://dfw.storage.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9", - "ord": "http://ord.storage.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9", - "sat": "http://ord.storage.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9"}, - "servers": - {"default": "dfw", - "dfw": "http://dfw.servers.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9", - "ord": "http://ord.servers.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9", - "sat": "http://ord.servers.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9"}} - -Lastly, the tokens themselves are stored as objects in the -`/.token_[0-f]` containers. The names of the objects are the -token strings themselves, such as `AUTH_tked86bbd01864458aa2bd746879438d5a`. -The exact `.token_[0-f]` container chosen is based on the final digit of the -token name, such as `.token_a` for the token -`AUTH_tked86bbd01864458aa2bd746879438d5a`. The contents of the token objects -are JSON dictionaries of the format:: - - {"account": , - "user": , - "account_id": , - "groups": , - "expires": } - -The `` is the auth service account's name for that token. The `` -is the user within the account for that token. The `` is the -same as the `X-Container-Meta-Account-Id` for the auth service's account, -as described above. The `` is the user's groups, as described -above with the user object. The "expires" value indicates when the token is no -longer valid, as compared to Python's time.time() value. - -Here's an example token object's JSON dictionary:: - - {"account": "test", - "user": "tester", - "account_id": "AUTH_8980f74b1cda41e483cbe0a925f448a9", - "groups": [{"name": "test:tester"}, {"name": "test"}, {"name": ".admin"}], - "expires": 1291273147.1624689} - -To easily map a user to an already issued token, the token name is stored in -the user object's `X-Object-Meta-Auth-Token` header. - -Here is an example full listing of an :: - - .account_id - AUTH_2282f516-559f-4966-b239-b5c88829e927 - AUTH_f6f57a3c-33b5-4e85-95a5-a801e67505c8 - AUTH_fea96a36-c177-4ca4-8c7e-b8c715d9d37b - .token_0 - .token_1 - .token_2 - .token_3 - .token_4 - .token_5 - .token_6 - AUTH_tk9d2941b13d524b268367116ef956dee6 - .token_7 - .token_8 - AUTH_tk93627c6324c64f78be746f1e6a4e3f98 - .token_9 - .token_a - .token_b - .token_c - .token_d - .token_e - AUTH_tk0d37d286af2c43ffad06e99112b3ec4e - .token_f - AUTH_tk766bbde93771489982d8dc76979d11cf - reseller - .services - reseller - test - .services - tester - tester3 - test2 - .services - tester2 diff --git a/doc/source/index.rst b/doc/source/index.rst deleted file mode 100644 index dca14e1..0000000 --- a/doc/source/index.rst +++ /dev/null @@ -1,188 +0,0 @@ -.. Swauth documentation master file, created by - sphinx-quickstart on Mon Feb 14 19:34:51 2011. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Swauth -====== - - Copyright (c) 2010-2012 OpenStack, LLC - - An Auth Service for Swift as WSGI Middleware that uses Swift itself as a - backing store. Docs at: https://swauth.readthedocs.io/ or ask in - #openstack-swauth on freenode IRC (archive: http://eavesdrop.openstack.org/irclogs/%23openstack-swauth/). - - Source available at: https://github.com/openstack/swauth - - See also https://github.com/openstack/keystone for the standard OpenStack - auth service. - -Overview --------- - -Before discussing how to install Swauth within a Swift system, it might help to understand how Swauth does it work first. - -1. Swauth is middleware installed in the Swift Proxy's WSGI pipeline. - -2. It intercepts requests to ``/auth/`` (by default). - -3. It also uses Swift's `authorize callback `_ and `acl callback `_ features to authorize Swift requests. - -4. Swauth will also make various internal calls to the Swift WSGI pipeline it's installed in to manipulate containers and objects within an ``AUTH_.auth`` (by default) Swift account. These containers and objects are what store account and user information. - -5. Instead of #4, Swauth can be configured to call out to another remote Swauth to perform #4 on its behalf (using the swauth_remote config value). - -6. When managing accounts and users with the various ``swauth-`` command line tools, these tools are actually just performing HTTP requests against the ``/auth/`` end point referenced in #2. You can make your own tools that use the same :ref:`API `. - -7. In the special case of creating a new account, Swauth will do its usual WSGI-internal requests as per #4 but will also call out to the Swift cluster to create the actual Swift account. - - a. This Swift cluster callout is an account PUT request to the URL defined by the ``swift_default_cluster`` config value. - - b. This callout end point is also saved when the account is created so that it can be given to the users of that account in the future. - - c. Sometimes, due to public/private network routing or firewalling, the URL Swauth should use should be different than the URL Swauth should give the users later. That is why the ``default_swift_cluster`` config value can accept two URLs (first is the one for users, second is the one for Swauth). - - d. Once an account is created, the URL given to users for that account will not change, even if the ``default_swift_cluster`` config value changes. This is so that you can use multiple clusters with the same Swauth system; ``default_swift_cluster`` just points to the one where you want new users to go. - - e. You can change the stored URL for an account if need be with the ``swauth-set-account-service`` command line tool or a POST request (see :ref:`API `). - - -Install -------- - -1) Install Swauth with ``sudo python setup.py install`` or ``sudo python - setup.py develop`` or via whatever packaging system you may be using. - -2) Alter your ``proxy-server.conf`` pipeline to have ``swauth`` instead of ``tempauth``: - - Was:: - - [pipeline:main] - pipeline = catch_errors cache tempauth proxy-server - - Change To:: - - [pipeline:main] - pipeline = catch_errors cache swauth proxy-server - -3) Add to your ``proxy-server.conf`` the section for the Swauth WSGI filter:: - - [filter:swauth] - use = egg:swauth#swauth - set log_name = swauth - super_admin_key = swauthkey - default_swift_cluster = - - The ``default_swift_cluster`` setting can be confusing. - - a. If you're using an all-in-one type configuration where everything will be run on the local host on port 8080, you can omit the ``default_swift_cluster`` completely and it will default to ``local#http://127.0.0.1:8080/v1``. - - b. If you're using a single Swift proxy you can just set the ``default_swift_cluster = cluster_name#https://:/v1`` and that URL will be given to users as well as used by Swauth internally. (Quick note: be sure the ``http`` vs. ``https`` is set right depending on if you're using SSL.) - - c. If you're using multiple Swift proxies behind a load balancer, you'll probably want ``default_swift_cluster = cluster_name#https://:/v1#http://127.0.0.1:/v1`` so that Swauth gives out the first URL but uses the second URL internally. Remember to double-check the ``http`` vs. ``https`` settings for each of the URLs; they might be different if you're terminating SSL at the load balancer. - - Also see the ``proxy-server.conf-sample`` for more config options, such as the ability to have a remote Swauth in a multiple Swift cluster configuration. - -4) Be sure your Swift proxy allows account management in the ``proxy-server.conf``:: - - [app:proxy-server] - ... - allow_account_management = true - - For greater security, you can leave this off any public proxies and just have one or two private proxies with it turned on. - -5) Restart your proxy server ``swift-init proxy reload`` - -6) Initialize the Swauth backing store in Swift ``swauth-prep -K swauthkey`` - -7) Add an account/user ``swauth-add-user -A http[s]://:/auth/ -K - swauthkey -a test tester testing`` - -8) Ensure it works ``swift -A http[s]://:/auth/v1.0 -U test:tester -K testing stat -v`` - - -If anything goes wrong, it's best to start checking the proxy server logs. The client command line utilities often don't get enough information to help. I will often just ``tail -F`` the appropriate proxy log (``/var/log/syslog`` or however you have it configured) and then run the Swauth command to see exactly what requests are happening to try to determine where things fail. - -General note, I find I occasionally just forget to reload the proxies after a config change; so that's the first thing you might try. Or, if you suspect the proxies aren't reloading properly, you might try ``swift-init proxy stop``, ensure all the processes died, then ``swift-init proxy start``. - -Also, it's quite common to get the ``/auth/v1.0`` vs. just ``/auth/`` URL paths confused. Usual rule is: Swauth tools use just ``/auth/`` and Swift tools use ``/auth/v1.0``. - - -Web Admin Install ------------------ - -1) If you installed from packages, you'll need to cd to the webadmin directory - the package installed. This is ``/usr/share/doc/python-swauth/webadmin`` - with the Lucid packages. If you installed from source, you'll need to cd to - the webadmin directory in the source directory. - -2) Upload the Web Admin files with ``swift -A http[s]://:/auth/v1.0 - -U .super_admin:.super_admin -K swauthkey upload .webadmin .`` - -3) Open ``http[s]://:/auth/`` in your browser. - - -Swift3 Middleware Compatibility -------------------------------- - - -`Swift3 middleware `_ support has to be -explicitly turned on in conf file using `s3_support` config option. It can -easily be used with swauth when `auth_type` in swauth is configured to be -*Plaintext* (default):: - - [pipeline:main] - pipeline = catch_errors cache swift3 swauth proxy-server - - [filter:swauth] - use = egg:swauth#swauth - super_admin_key = swauthkey - s3_support = on - -The AWS S3 client uses password in plaintext to -`compute HMAC signature `_ -When `auth_type` in swauth is configured to be *Sha1* or *Sha512*, swauth -can only use the stored hashed password to compute HMAC signature. This results -in signature mismatch although the user credentials are correct. - -When `auth_type` is **not** *Plaintext*, the only way for S3 clients to -authenticate is by giving SHA1/SHA512 of password as input to it's HMAC -function. In this case, the S3 clients will have to know `auth_type` and -`auth_type_salt` beforehand. Here is a sample configuration:: - - [pipeline:main] - pipeline = catch_errors cache swift3 swauth proxy-server - - [filter:swauth] - use = egg:swauth#swauth - super_admin_key = swauthkey - s3_support = on - auth_type = Sha512 - auth_type_salt = mysalt - -**Security Concern**: Swauth stores user information (username, password hash, -salt etc) as objects in the Swift cluster. If these backend objects which -contain password hashes gets stolen, the intruder will be able to authenticate -using the hash directly when S3 API is used. - - -Contents --------- - -.. toctree:: - :maxdepth: 2 - - license - details - swauth - middleware - api - authtypes - - -Indices and tables ------------------- - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/doc/source/license.rst b/doc/source/license.rst deleted file mode 100644 index 590a9b4..0000000 --- a/doc/source/license.rst +++ /dev/null @@ -1,225 +0,0 @@ -.. _license: - -******* -LICENSE -******* - -:: - - Copyright (c) 2010-2011 OpenStack, LLC - - 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. - - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 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. diff --git a/doc/source/middleware.rst b/doc/source/middleware.rst deleted file mode 100644 index a25acd4..0000000 --- a/doc/source/middleware.rst +++ /dev/null @@ -1,9 +0,0 @@ -.. _swauth_middleware_module: - -swauth.middleware -================= - -.. automodule:: swauth.middleware - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/source/swauth.rst b/doc/source/swauth.rst deleted file mode 100644 index c50c350..0000000 --- a/doc/source/swauth.rst +++ /dev/null @@ -1,9 +0,0 @@ -.. _swauth_module: - -swauth -====== - -.. automodule:: swauth - :members: - :undoc-members: - :show-inheritance: diff --git a/etc/proxy-server.conf-sample b/etc/proxy-server.conf-sample deleted file mode 100644 index db44bcd..0000000 --- a/etc/proxy-server.conf-sample +++ /dev/null @@ -1,86 +0,0 @@ -[DEFAULT] -# Standard from Swift - -[pipeline:main] -# Standard from Swift, this is just an example of where to put swauth -pipeline = catch_errors healthcheck cache ratelimit swauth proxy-server - -[app:proxy-server] -# Standard from Swift, main point to note is the inclusion of -# allow_account_management = true (only for the proxy servers where you want to -# be able to create/delete accounts). -use = egg:swift#proxy -allow_account_management = true - -[filter:swauth] -use = egg:swauth#swauth -# You can override the default log routing for this filter here: -# set log_name = swauth -# set log_facility = LOG_LOCAL0 -# set log_level = INFO -# set log_headers = False -# set log_address = /dev/log -# The reseller prefix will verify a token begins with this prefix before even -# attempting to validate it. Also, with authorization, only Swift storage -# accounts with this prefix will be authorized by this middleware. Useful if -# multiple auth systems are in use for one Swift cluster. -# reseller_prefix = AUTH -# If you wish to use a Swauth service on a remote cluster with this cluster: -# swauth_remote = http://remotehost:port/auth -# swauth_remote_timeout = 10 -# When using swauth_remote, the rest of these settings have no effect. -# -# The auth prefix will cause requests beginning with this prefix to be routed -# to the auth subsystem, for granting tokens, creating accounts, users, etc. -# auth_prefix = /auth/ -# Cluster strings are of the format name#url where name is a short name for the -# Swift cluster and url is the url to the proxy server(s) for the cluster. -# default_swift_cluster = local#http://127.0.0.1:8080/v1 -# You may also use the format name#url#url where the first url is the one -# given to users to access their account (public url) and the second is the one -# used by swauth itself to create and delete accounts (private url). This is -# useful when a load balancer url should be used by users, but swauth itself is -# behind the load balancer. Example: -# default_swift_cluster = local#https://public.com:8080/v1#http://private.com:8080/v1 -# Number of seconds a newly issued token should be valid for, by default. -# token_life = 86400 -# Maximum number of seconds a newly issued token can be valid for. -# max_token_life = -# Specifies how the user key is stored. The default is 'plaintext', leaving the -# key unsecured but available for key-signing features if such are ever added. -# An alternative is 'sha512' which stores only a one-way hash of the key leaving -# it secure but unavailable for key-signing. -# auth_type = plaintext -# Used if the auth_type is sha1 or sha512. Salt is data(text) that is used as -# an additional input to the one-way encoding function. If not set, a random -# salt will be generated for each password. -# auth_type_salt = -# This allows middleware higher in the WSGI pipeline to override auth -# processing, useful for middleware such as tempurl and formpost. If you know -# you're not going to use such middleware and you want a bit of extra security, -# you can set this to false. -# allow_overrides = true -# This allows swauth to PUT authentication related objects over a specific -# storage policy instead of the default one. When this is set, all requests -# sent by swauth will contain X-Storage-Policy header with its value set -# to the value specified here. -# default_storage_policy = -# Highly recommended to change this. If you comment this out, the Swauth -# administration features will be disabled for this proxy. -super_admin_key = swauthkey - -[filter:ratelimit] -# Standard from Swift -use = egg:swift#ratelimit - -[filter:cache] -# Standard from Swift -use = egg:swift#memcache - -[filter:healthcheck] -# Standard from Swift -use = egg:swift#healthcheck - -[filter:catch_errors] -# Standard from Swift -use = egg:swift#catch_errors diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index dead155..0000000 --- a/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. - -eventlet!=0.18.3,!=0.20.1,>=0.18.2 # MIT -python-swiftclient>=3.2.0 # Apache-2.0 -six>=1.10.0 # MIT diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 004a22e..0000000 --- a/setup.cfg +++ /dev/null @@ -1,63 +0,0 @@ -[metadata] -name = swauth -summary = An alternative authentication system for Swift -description-file = - README.md -author = OpenStack -author-email = openstack-discuss@lists.openstack.org -home-page = https://github.com/openstack/swauth -classifier = - Development Status :: 5 - Production/Stable - Environment :: OpenStack - Intended Audience :: Information Technology - Intended Audience :: System Administrators - License :: OSI Approved :: Apache Software License - Operating System :: POSIX :: Linux - Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 - -[pbr] -skip_authors = True -skip_changelog = True - -[files] -packages = - swauth -scripts = - bin/swauth-add-account - bin/swauth-add-user - bin/swauth-cleanup-tokens - bin/swauth-delete-account - bin/swauth-delete-user - bin/swauth-list - bin/swauth-prep - bin/swauth-set-account-service - -[entry_points] -paste.filter_factory = - swauth = swauth.middleware:filter_factory - -[build_sphinx] -all_files = 1 -build-dir = doc/build -source-dir = doc/source - -[egg_info] -tag_build = -tag_date = 0 -tag_svn_revision = 0 - -[compile_catalog] -directory = swauth/locale -domain = swauth - -[update_catalog] -domain = swauth -output_dir = swauth/locale -input_file = swauth/locale/swauth.pot - -[extract_messages] -keywords = _ l_ lazy_gettext -mapping_file = babel.cfg -output_file = swauth/locale/swauth.pot diff --git a/setup.py b/setup.py deleted file mode 100644 index 566d844..0000000 --- a/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. -# -# 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. - -# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT -import setuptools - -# In python < 2.7.4, a lazy loading of package `pbr` will break -# setuptools if some other modules registered functions in `atexit`. -# solution from: http://bugs.python.org/issue15881#msg170215 -try: - import multiprocessing # noqa -except ImportError: - pass - -setuptools.setup( - setup_requires=['pbr>=2.0.0'], - pbr=True) diff --git a/swauth/__init__.py b/swauth/__init__.py deleted file mode 100644 index 47f805b..0000000 --- a/swauth/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2010-2013 OpenStack, LLC. -# -# 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 gettext -import pkg_resources - - -try: - # First, try to get our version out of PKG-INFO. If we're installed, - # this'll let us find our version without pulling in pbr. After all, if - # we're installed on a system, we're not in a Git-managed source tree, so - # pbr doesn't really buy us anything. - __version__ = pkg_resources.get_provider( - pkg_resources.Requirement.parse('swauth')).version -except pkg_resources.DistributionNotFound: - # No PKG-INFO? We're probably running from a checkout, then. Let pbr do - # its thing to figure out a version number. - import pbr.version - __version__ = pbr.version.VersionInfo( - 'swauth').version_string() - -gettext.install('swauth') diff --git a/swauth/authtypes.py b/swauth/authtypes.py deleted file mode 100644 index 7dbc175..0000000 --- a/swauth/authtypes.py +++ /dev/null @@ -1,238 +0,0 @@ -# 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. -# -# Pablo Llopis 2011 - - -"""This module hosts available auth types for encoding and matching user keys. -For adding a new auth type, simply write a class that satisfies the following -conditions: - -- For the class name, capitalize first letter only. This makes sure the user - can specify an all-lowercase config option such as "plaintext" or "sha1". - Swauth takes care of capitalizing the first letter before instantiating it. -- Write an encode(key) method that will take a single argument, the user's key, - and returns the encoded string. For plaintext, this would be - "plaintext:" -- Write a match(key, creds) method that will take two arguments: the user's - key, and the user's retrieved credentials. Return a boolean value that - indicates whether the match is True or False. -""" - -import hashlib -import os -import string -import sys - - -#: Maximum length any valid token should ever be. -MAX_TOKEN_LENGTH = 5000 - - -def validate_creds(creds): - """Parse and validate user credentials whether format is right - - :param creds: User credentials - :returns: Auth_type class instance and parsed user credentials in dict - :raises ValueError: If credential format is wrong (eg: bad auth_type) - """ - try: - auth_type, auth_rest = creds.split(':', 1) - except ValueError: - raise ValueError("Missing ':' in %s" % creds) - authtypes = sys.modules[__name__] - auth_encoder = getattr(authtypes, auth_type.title(), None) - if auth_encoder is None: - raise ValueError('Invalid auth_type: %s' % auth_type) - auth_encoder = auth_encoder() - parsed_creds = dict(type=auth_type, salt=None, hash=None) - parsed_creds.update(auth_encoder.validate(auth_rest)) - return auth_encoder, parsed_creds - - -class Plaintext(object): - """Provides a particular auth type for encoding format for encoding and - matching user keys. - - This class must be all lowercase except for the first character, which - must be capitalized. encode and match methods must be provided and are - the only ones that will be used by swauth. - """ - def encode(self, key): - """Encodes a user key into a particular format. The result of this method - will be used by swauth for storing user credentials. - - :param key: User's secret key - :returns: A string representing user credentials - """ - return "plaintext:%s" % key - - def match(self, key, creds, **kwargs): - """Checks whether the user-provided key matches the user's credentials - - :param key: User-supplied key - :param creds: User's stored credentials - :param kwargs: Extra keyword args for compatibility reason with - other auth_type classes - :returns: True if the supplied key is valid, False otherwise - """ - return self.encode(key) == creds - - def validate(self, auth_rest): - """Validate user credentials whether format is right for Plaintext - - :param auth_rest: User credentials' part without auth_type - :return: Dict with a hash part of user credentials - :raises ValueError: If credentials' part has zero length - """ - if len(auth_rest) == 0: - raise ValueError("Key must have non-zero length!") - return dict(hash=auth_rest) - - -class Sha1(object): - """Provides a particular auth type for encoding format for encoding and - matching user keys. - - This class must be all lowercase except for the first character, which - must be capitalized. encode and match methods must be provided and are - the only ones that will be used by swauth. - """ - - def encode_w_salt(self, salt, key): - """Encodes a user key with salt into a particular format. The result of - this method will be used internally. - - :param salt: Salt for hashing - :param key: User's secret key - :returns: A string representing user credentials - """ - enc_key = '%s%s' % (salt, key) - enc_val = hashlib.sha1(enc_key).hexdigest() - return "sha1:%s$%s" % (salt, enc_val) - - def encode(self, key): - """Encodes a user key into a particular format. The result of this method - will be used by swauth for storing user credentials. - - If salt is not manually set in conf file, a random salt will be - generated and used. - - :param key: User's secret key - :returns: A string representing user credentials - """ - salt = self.salt or os.urandom(32).encode('base64').rstrip() - return self.encode_w_salt(salt, key) - - def match(self, key, creds, salt, **kwargs): - """Checks whether the user-provided key matches the user's credentials - - :param key: User-supplied key - :param creds: User's stored credentials - :param salt: Salt for hashing - :param kwargs: Extra keyword args for compatibility reason with - other auth_type classes - :returns: True if the supplied key is valid, False otherwise - """ - return self.encode_w_salt(salt, key) == creds - - def validate(self, auth_rest): - """Validate user credentials whether format is right for Sha1 - - :param auth_rest: User credentials' part without auth_type - :return: Dict with a hash and a salt part of user credentials - :raises ValueError: If credentials' part doesn't contain delimiter - between a salt and a hash. - """ - try: - auth_salt, auth_hash = auth_rest.split('$') - except ValueError: - raise ValueError("Missing '$' in %s" % auth_rest) - - if len(auth_salt) == 0: - raise ValueError("Salt must have non-zero length!") - if len(auth_hash) != 40: - raise ValueError("Hash must have 40 chars!") - if not all(c in string.hexdigits for c in auth_hash): - raise ValueError("Hash must be hexadecimal!") - - return dict(salt=auth_salt, hash=auth_hash) - - -class Sha512(object): - """Provides a particular auth type for encoding format for encoding and - matching user keys. - - This class must be all lowercase except for the first character, which - must be capitalized. encode and match methods must be provided and are - the only ones that will be used by swauth. - """ - - def encode_w_salt(self, salt, key): - """Encodes a user key with salt into a particular format. The result of - this method will be used internal. - - :param salt: Salt for hashing - :param key: User's secret key - :returns: A string representing user credentials - """ - enc_key = '%s%s' % (salt, key) - enc_val = hashlib.sha512(enc_key).hexdigest() - return "sha512:%s$%s" % (salt, enc_val) - - def encode(self, key): - """Encodes a user key into a particular format. The result of this method - will be used by swauth for storing user credentials. - - If salt is not manually set in conf file, a random salt will be - generated and used. - - :param key: User's secret key - :returns: A string representing user credentials - """ - salt = self.salt or os.urandom(32).encode('base64').rstrip() - return self.encode_w_salt(salt, key) - - def match(self, key, creds, salt, **kwargs): - """Checks whether the user-provided key matches the user's credentials - - :param key: User-supplied key - :param creds: User's stored credentials - :param salt: Salt for hashing - :param kwargs: Extra keyword args for compatibility reason with - other auth_type classes - :returns: True if the supplied key is valid, False otherwise - """ - return self.encode_w_salt(salt, key) == creds - - def validate(self, auth_rest): - """Validate user credentials whether format is right for Sha512 - - :param auth_rest: User credentials' part without auth_type - :return: Dict with a hash and a salt part of user credentials - :raises ValueError: If credentials' part doesn't contain delimiter - between a salt and a hash. - """ - try: - auth_salt, auth_hash = auth_rest.split('$') - except ValueError: - raise ValueError("Missing '$' in %s" % auth_rest) - - if len(auth_salt) == 0: - raise ValueError("Salt must have non-zero length!") - if len(auth_hash) != 128: - raise ValueError("Hash must have 128 chars!") - if not all(c in string.hexdigits for c in auth_hash): - raise ValueError("Hash must be hexadecimal!") - - return dict(salt=auth_salt, hash=auth_hash) diff --git a/swauth/middleware.py b/swauth/middleware.py deleted file mode 100644 index d0f97ac..0000000 --- a/swauth/middleware.py +++ /dev/null @@ -1,1709 +0,0 @@ -# Copyright (c) 2010-2012 OpenStack, LLC. -# -# 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 base64 -from hashlib import sha1 -from hashlib import sha512 -import hmac -from httplib import HTTPConnection -from httplib import HTTPSConnection -import json -import six -from six.moves.urllib.parse import urlparse -import swift -from time import gmtime -from time import strftime -from time import time -from traceback import format_exc -from urllib import quote -from urllib import unquote -from uuid import uuid4 - -from eventlet.timeout import Timeout -from eventlet import TimeoutError -from swift.common.swob import HTTPAccepted -from swift.common.swob import HTTPBadRequest -from swift.common.swob import HTTPConflict -from swift.common.swob import HTTPCreated -from swift.common.swob import HTTPForbidden -from swift.common.swob import HTTPMethodNotAllowed -from swift.common.swob import HTTPMovedPermanently -from swift.common.swob import HTTPNoContent -from swift.common.swob import HTTPNotFound -from swift.common.swob import HTTPUnauthorized -from swift.common.swob import Request -from swift.common.swob import Response - -from swift.common.bufferedhttp import http_connect_raw as http_connect -from swift.common.middleware.acl import clean_acl -from swift.common.middleware.acl import parse_acl -from swift.common.middleware.acl import referrer_allowed -from swift.common.utils import cache_from_env -from swift.common.utils import get_logger -from swift.common.utils import get_remote_client -from swift.common.utils import HASH_PATH_PREFIX -from swift.common.utils import HASH_PATH_SUFFIX -from swift.common.utils import split_path -from swift.common.utils import TRUE_VALUES -import swift.common.wsgi - -import swauth.authtypes -from swauth import swift_version - - -SWIFT_MIN_VERSION = "2.2.0" -CONTENT_TYPE_JSON = 'application/json' - - -class Swauth(object): - """Scalable authentication and authorization system that uses Swift as its - backing store. - - :param app: The next WSGI app in the pipeline - :param conf: The dict of configuration values - """ - - def __init__(self, app, conf): - self.app = app - self.conf = conf - self.logger = get_logger(conf, log_route='swauth') - if not swift_version.at_least(SWIFT_MIN_VERSION): - msg = ("Your Swift installation is too old (%s). You need at " - "least %s." % (swift.__version__, SWIFT_MIN_VERSION)) - self.logger.critical(msg) - raise ValueError(msg) - self.log_headers = conf.get('log_headers', 'no').lower() in TRUE_VALUES - self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip() - if self.reseller_prefix and self.reseller_prefix[-1] != '_': - self.reseller_prefix += '_' - self.auth_prefix = conf.get('auth_prefix', '/auth/') - if not self.auth_prefix: - self.auth_prefix = '/auth/' - if self.auth_prefix[0] != '/': - self.auth_prefix = '/' + self.auth_prefix - if self.auth_prefix[-1] != '/': - self.auth_prefix += '/' - self.swauth_remote = conf.get('swauth_remote') - if self.swauth_remote: - self.swauth_remote = self.swauth_remote.rstrip('/') - if not self.swauth_remote: - msg = _('Invalid swauth_remote set in conf file! Exiting.') - self.logger.critical(msg) - raise ValueError(msg) - self.swauth_remote_parsed = urlparse(self.swauth_remote) - if self.swauth_remote_parsed.scheme not in ('http', 'https'): - msg = _('Cannot handle protocol scheme %(schema)s ' - 'for url %(url)s!') % \ - (self.swauth_remote_parsed.scheme, repr(self.swauth_remote)) - self.logger.critical(msg) - raise ValueError(msg) - self.swauth_remote_timeout = int(conf.get('swauth_remote_timeout', 10)) - self.auth_account = '%s.auth' % self.reseller_prefix - self.default_swift_cluster = conf.get('default_swift_cluster', - 'local#http://127.0.0.1:8080/v1') - # This setting is a little messy because of the options it has to - # provide. The basic format is cluster_name#url, such as the default - # value of local#http://127.0.0.1:8080/v1. - # If the URL given to the user needs to differ from the url used by - # Swauth to create/delete accounts, there's a more complex format: - # cluster_name#url#url, such as - # local#https://public.com:8080/v1#http://private.com:8080/v1. - cluster_parts = self.default_swift_cluster.split('#', 2) - self.dsc_name = cluster_parts[0] - if len(cluster_parts) == 3: - self.dsc_url = cluster_parts[1].rstrip('/') - self.dsc_url2 = cluster_parts[2].rstrip('/') - elif len(cluster_parts) == 2: - self.dsc_url = self.dsc_url2 = cluster_parts[1].rstrip('/') - else: - raise ValueError('Invalid cluster format') - self.dsc_parsed = urlparse(self.dsc_url) - if self.dsc_parsed.scheme not in ('http', 'https'): - raise ValueError('Cannot handle protocol scheme %s for url %s' % - (self.dsc_parsed.scheme, repr(self.dsc_url))) - self.dsc_parsed2 = urlparse(self.dsc_url2) - if self.dsc_parsed2.scheme not in ('http', 'https'): - raise ValueError('Cannot handle protocol scheme %s for url %s' % - (self.dsc_parsed2.scheme, repr(self.dsc_url2))) - self.super_admin_key = conf.get('super_admin_key') - if not self.super_admin_key and not self.swauth_remote: - msg = _('No super_admin_key set in conf file; Swauth ' - 'administration features will be disabled.') - self.logger.warning(msg) - self.token_life = int(conf.get('token_life', 86400)) - self.max_token_life = int(conf.get('max_token_life', self.token_life)) - self.timeout = int(conf.get('node_timeout', 10)) - self.itoken = None - self.itoken_expires = None - self.allowed_sync_hosts = [h.strip() - for h in conf.get('allowed_sync_hosts', '127.0.0.1').split(',') - if h.strip()] - # Get an instance of our auth_type encoder for saving and checking the - # user's key - self.auth_type = conf.get('auth_type', 'Plaintext').title() - self.auth_encoder = getattr(swauth.authtypes, self.auth_type, None) - if self.auth_encoder is None: - raise ValueError('Invalid auth_type in config file: %s' - % self.auth_type) - # If auth_type_salt is not set in conf file, a random salt will be - # generated for each new password to be encoded. - self.auth_encoder.salt = conf.get('auth_type_salt', None) - - # Due to security concerns, S3 support is disabled by default. - self.s3_support = conf.get('s3_support', 'off').lower() in TRUE_VALUES - if self.s3_support and self.auth_type != 'Plaintext' \ - and not self.auth_encoder.salt: - msg = _('S3 support requires salt to be manually set in conf ' - 'file using auth_type_salt config option.') - self.logger.warning(msg) - self.s3_support = False - - self.allow_overrides = \ - conf.get('allow_overrides', 't').lower() in TRUE_VALUES - self.agent = '%(orig)s Swauth' - self.swift_source = 'SWTH' - self.default_storage_policy = conf.get('default_storage_policy', None) - - def make_pre_authed_request(self, env, method=None, path=None, body=None, - headers=None): - """Nearly the same as swift.common.wsgi.make_pre_authed_request - except that this also always sets the 'swift.source' and user - agent. - - Newer Swift code will support swift_source as a kwarg, but we - do it this way so we don't have to have a newer Swift. - - Since we're doing this anyway, we may as well set the user - agent too since we always do that. - """ - if self.default_storage_policy: - sp = self.default_storage_policy - if headers: - headers.update({'X-Storage-Policy': sp}) - else: - headers = {'X-Storage-Policy': sp} - subreq = swift.common.wsgi.make_pre_authed_request( - env, method=method, path=path, body=body, headers=headers, - agent=self.agent) - subreq.environ['swift.source'] = self.swift_source - return subreq - - def __call__(self, env, start_response): - """Accepts a standard WSGI application call, authenticating the request - and installing callback hooks for authorization and ACL header - validation. For an authenticated request, REMOTE_USER will be set to a - comma separated list of the user's groups. - - With a non-empty reseller prefix, acts as the definitive auth service - for just tokens and accounts that begin with that prefix, but will deny - requests outside this prefix if no other auth middleware overrides it. - - With an empty reseller prefix, acts as the definitive auth service only - for tokens that validate to a non-empty set of groups. For all other - requests, acts as the fallback auth service when no other auth - middleware overrides it. - - Alternatively, if the request matches the self.auth_prefix, the request - will be routed through the internal auth request handler (self.handle). - This is to handle creating users, accounts, granting tokens, etc. - """ - if 'keystone.identity' in env: - return self.app(env, start_response) - # We're going to consider OPTIONS requests harmless and the CORS - # support in the Swift proxy needs to get them. - if env.get('REQUEST_METHOD') == 'OPTIONS': - return self.app(env, start_response) - if self.allow_overrides and env.get('swift.authorize_override', False): - return self.app(env, start_response) - if not self.swauth_remote: - if env.get('PATH_INFO', '') == self.auth_prefix[:-1]: - return HTTPMovedPermanently(add_slash=True)(env, - start_response) - elif env.get('PATH_INFO', '').startswith(self.auth_prefix): - return self.handle(env, start_response) - s3 = env.get('swift3.auth_details') - if s3 and not self.s3_support: - msg = 'S3 support is disabled in swauth.' - return HTTPBadRequest(body=msg)(env, start_response) - token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) - if token and len(token) > swauth.authtypes.MAX_TOKEN_LENGTH: - return HTTPBadRequest(body='Token exceeds maximum length.')(env, - start_response) - if s3 or (token and token.startswith(self.reseller_prefix)): - # Note: Empty reseller_prefix will match all tokens. - groups = self.get_groups(env, token) - if groups: - env['REMOTE_USER'] = groups - user = groups and groups.split(',', 1)[0] or '' - # We know the proxy logs the token, so we augment it just a bit - # to also log the authenticated user. - env['HTTP_X_AUTH_TOKEN'] = \ - '%s,%s' % (user, 's3' if s3 else token) - env['swift.authorize'] = self.authorize - env['swift.clean_acl'] = clean_acl - if '.reseller_admin' in groups: - env['reseller_request'] = True - else: - # Unauthorized token - if self.reseller_prefix and token and \ - token.startswith(self.reseller_prefix): - # Because I know I'm the definitive auth for this token, I - # can deny it outright. - return HTTPUnauthorized()(env, start_response) - # Because I'm not certain if I'm the definitive auth, I won't - # overwrite swift.authorize and I'll just set a delayed denial - # if nothing else overrides me. - elif 'swift.authorize' not in env: - env['swift.authorize'] = self.denied_response - else: - if self.reseller_prefix: - # With a non-empty reseller_prefix, I would like to be called - # back for anonymous access to accounts I know I'm the - # definitive auth for. - try: - version, rest = split_path(env.get('PATH_INFO', ''), - 1, 2, True) - except ValueError: - rest = None - if rest and rest.startswith(self.reseller_prefix): - # Handle anonymous access to accounts I'm the definitive - # auth for. - env['swift.authorize'] = self.authorize - env['swift.clean_acl'] = clean_acl - # Not my token, not my account, I can't authorize this request, - # deny all is a good idea if not already set... - elif 'swift.authorize' not in env: - env['swift.authorize'] = self.denied_response - # Because I'm not certain if I'm the definitive auth for empty - # reseller_prefixed accounts, I won't overwrite swift.authorize. - elif 'swift.authorize' not in env: - env['swift.authorize'] = self.authorize - env['swift.clean_acl'] = clean_acl - return self.app(env, start_response) - - def _get_concealed_token(self, token): - """Returns hashed token to be used as object name in Swift. - - Tokens are stored in auth account but object names are visible in Swift - logs. Object names are hashed from token. - """ - enc_key = "%s:%s:%s" % (HASH_PATH_PREFIX, token, HASH_PATH_SUFFIX) - return sha512(enc_key).hexdigest() - - def get_groups(self, env, token): - """Get groups for the given token. - - :param env: The current WSGI environment dictionary. - :param token: Token to validate and return a group string for. - - :returns: None if the token is invalid or a string containing a comma - separated list of groups the authenticated user is a member - of. The first group in the list is also considered a unique - identifier for that user. - """ - groups = None - memcache_client = cache_from_env(env) - if memcache_client: - memcache_key = '%s/auth/%s' % (self.reseller_prefix, token) - cached_auth_data = memcache_client.get(memcache_key) - if cached_auth_data: - expires, groups = cached_auth_data - if expires < time(): - groups = None - - s3_auth_details = env.get('swift3.auth_details') - if s3_auth_details: - if not self.s3_support: - self.logger.warning('S3 support is disabled in swauth.') - return None - if self.swauth_remote: - # TODO(gholt): Support S3-style authorization with - # swauth_remote mode - self.logger.warning('S3-style authorization not supported yet ' - 'with swauth_remote mode.') - return None - try: - account, user = s3_auth_details['access_key'].split(':', 1) - signature_from_user = s3_auth_details['signature'] - msg = s3_auth_details['string_to_sign'] - except Exception: - self.logger.debug( - 'Swauth cannot parse swift3.auth_details value %r' % - (s3_auth_details, )) - return None - path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user)) - resp = self.make_pre_authed_request( - env, 'GET', path).get_response(self.app) - if resp.status_int // 100 != 2: - return None - - if 'x-object-meta-account-id' in resp.headers: - account_id = resp.headers['x-object-meta-account-id'] - else: - path = quote('/v1/%s/%s' % (self.auth_account, account)) - resp2 = self.make_pre_authed_request( - env, 'HEAD', path).get_response(self.app) - if resp2.status_int // 100 != 2: - return None - account_id = resp2.headers['x-container-meta-account-id'] - - path = env['PATH_INFO'] - env['PATH_INFO'] = path.replace("%s:%s" % (account, user), - account_id, 1) - detail = json.loads(resp.body) - if detail: - creds = detail.get('auth') - try: - auth_encoder, creds_dict = \ - swauth.authtypes.validate_creds(creds) - except ValueError as e: - self.logger.error('%s' % e.args[0]) - return None - - password = creds_dict['hash'] - - # https://bugs.python.org/issue5285 - if isinstance(password, six.text_type): - password = password.encode('utf-8') - if isinstance(msg, six.text_type): - msg = msg.encode('utf-8') - - valid_signature = base64.encodestring(hmac.new( - password, msg, sha1).digest()).strip() - if signature_from_user != valid_signature: - return None - groups = [g['name'] for g in detail['groups']] - if '.admin' in groups: - groups.remove('.admin') - groups.append(account_id) - groups = ','.join(groups) - return groups - - if not groups: - if self.swauth_remote: - with Timeout(self.swauth_remote_timeout): - conn = http_connect(self.swauth_remote_parsed.hostname, - self.swauth_remote_parsed.port, 'GET', - '%s/v2/.token/%s' % (self.swauth_remote_parsed.path, - quote(token)), - ssl=(self.swauth_remote_parsed.scheme == 'https')) - resp = conn.getresponse() - resp.read() - conn.close() - if resp.status // 100 != 2: - return None - expires_from_now = float(resp.getheader('x-auth-ttl')) - groups = resp.getheader('x-auth-groups') - if memcache_client: - memcache_client.set( - memcache_key, (time() + expires_from_now, groups), - time=expires_from_now) - else: - object_name = self._get_concealed_token(token) - path = quote('/v1/%s/.token_%s/%s' % - (self.auth_account, object_name[-1], object_name)) - resp = self.make_pre_authed_request( - env, 'GET', path).get_response(self.app) - if resp.status_int // 100 != 2: - return None - detail = json.loads(resp.body) - if detail['expires'] < time(): - self.make_pre_authed_request( - env, 'DELETE', path).get_response(self.app) - return None - groups = [g['name'] for g in detail['groups']] - if '.admin' in groups: - groups.remove('.admin') - groups.append(detail['account_id']) - groups = ','.join(groups) - if memcache_client: - memcache_client.set( - memcache_key, - (detail['expires'], groups), - time=float(detail['expires'] - time())) - return groups - - def authorize(self, req): - """Returns None if the request is authorized to continue or a standard - WSGI response callable if not. - """ - try: - version, account, container, obj = split_path(req.path, 1, 4, True) - except ValueError: - return HTTPNotFound(request=req) - if not account or not account.startswith(self.reseller_prefix): - return self.denied_response(req) - user_groups = (req.remote_user or '').split(',') - if '.reseller_admin' in user_groups and \ - account != self.reseller_prefix and \ - account[len(self.reseller_prefix)] != '.': - req.environ['swift_owner'] = True - return None - if account in user_groups and \ - (req.method not in ('DELETE', 'PUT') or container): - # If the user is admin for the account and is not trying to do an - # account DELETE or PUT... - req.environ['swift_owner'] = True - return None - if (req.environ.get('swift_sync_key') and - req.environ['swift_sync_key'] == - req.headers.get('x-container-sync-key', None) and - 'x-timestamp' in req.headers and - (req.remote_addr in self.allowed_sync_hosts or - get_remote_client(req) in self.allowed_sync_hosts)): - return None - referrers, groups = parse_acl(getattr(req, 'acl', None)) - if referrer_allowed(req.referer, referrers): - if obj or '.rlistings' in groups: - return None - return self.denied_response(req) - if not req.remote_user: - return self.denied_response(req) - for user_group in user_groups: - if user_group in groups: - return None - return self.denied_response(req) - - def denied_response(self, req): - """Returns a standard WSGI response callable with the status of 403 or 401 - depending on whether the REMOTE_USER is set or not. - """ - if not hasattr(req, 'credentials_valid'): - req.credentials_valid = None - if req.remote_user or req.credentials_valid: - return HTTPForbidden(request=req) - else: - return HTTPUnauthorized(request=req) - - def handle(self, env, start_response): - """WSGI entry point for auth requests (ones that match the - self.auth_prefix). - Wraps env in swob.Request object and passes it down. - - :param env: WSGI environment dictionary - :param start_response: WSGI callable - """ - try: - req = Request(env) - if self.auth_prefix: - req.path_info_pop() - req.bytes_transferred = '-' - req.client_disconnect = False - if 'x-storage-token' in req.headers and \ - 'x-auth-token' not in req.headers: - req.headers['x-auth-token'] = req.headers['x-storage-token'] - if 'eventlet.posthooks' in env: - env['eventlet.posthooks'].append( - (self.posthooklogger, (req,), {})) - return self.handle_request(req)(env, start_response) - else: - # Lack of posthook support means that we have to log on the - # start of the response, rather than after all the data has - # been sent. This prevents logging client disconnects - # differently than full transmissions. - response = self.handle_request(req)(env, start_response) - self.posthooklogger(env, req) - return response - except (Exception, TimeoutError): - print("EXCEPTION IN handle: %s: %s" % (format_exc(), env)) - start_response('500 Server Error', - [('Content-Type', 'text/plain')]) - return ['Internal server error.\n'] - - def handle_request(self, req): - """Entry point for auth requests (ones that match the self.auth_prefix). - Should return a WSGI-style callable (such as swob.Response). - - :param req: swob.Request object - """ - req.start_time = time() - handler = None - try: - version, account, user, _junk = split_path(req.path_info, - minsegs=0, maxsegs=4, rest_with_last=True) - except ValueError: - return HTTPNotFound(request=req) - if version in ('v1', 'v1.0', 'auth'): - if req.method == 'GET': - handler = self.handle_get_token - elif version == 'v2': - if not self.super_admin_key: - return HTTPNotFound(request=req) - req.path_info_pop() - if req.method == 'GET': - if not account and not user: - handler = self.handle_get_reseller - elif account: - if not user: - handler = self.handle_get_account - elif account == '.token': - req.path_info_pop() - handler = self.handle_validate_token - else: - handler = self.handle_get_user - elif req.method == 'PUT': - if not user: - handler = self.handle_put_account - else: - handler = self.handle_put_user - elif req.method == 'DELETE': - if not user: - handler = self.handle_delete_account - else: - handler = self.handle_delete_user - elif req.method == 'POST': - if account == '.prep': - handler = self.handle_prep - elif user == '.services': - handler = self.handle_set_services - else: - handler = self.handle_webadmin - if not handler: - req.response = HTTPBadRequest(request=req) - else: - req.response = handler(req) - return req.response - - def handle_webadmin(self, req): - if req.method not in ('GET', 'HEAD'): - return HTTPMethodNotAllowed(request=req) - subpath = req.path[len(self.auth_prefix):] or 'index.html' - path = quote('/v1/%s/.webadmin/%s' % (self.auth_account, subpath)) - req.response = self.make_pre_authed_request( - req.environ, req.method, path).get_response(self.app) - return req.response - - def handle_prep(self, req): - """Handles the POST v2/.prep call for preparing the backing store Swift - cluster for use with the auth subsystem. Can only be called by - .super_admin. - - :param req: The swob.Request to process. - :returns: swob.Response, 204 on success - """ - if not self.is_super_admin(req): - return self.denied_response(req) - path = quote('/v1/%s' % self.auth_account) - resp = self.make_pre_authed_request( - req.environ, 'PUT', path).get_response(self.app) - if resp.status_int // 100 != 2: - raise Exception('Could not create the main auth account: %s %s' % - (path, resp.status)) - path = quote('/v1/%s/.account_id' % self.auth_account) - resp = self.make_pre_authed_request( - req.environ, 'PUT', path).get_response(self.app) - if resp.status_int // 100 != 2: - raise Exception('Could not create container: %s %s' % - (path, resp.status)) - for container in xrange(16): - path = quote('/v1/%s/.token_%x' % (self.auth_account, container)) - resp = self.make_pre_authed_request( - req.environ, 'PUT', path).get_response(self.app) - if resp.status_int // 100 != 2: - raise Exception('Could not create container: %s %s' % - (path, resp.status)) - return HTTPNoContent(request=req) - - def handle_get_reseller(self, req): - """Handles the GET v2 call for getting general reseller information - (currently just a list of accounts). Can only be called by a - .reseller_admin. - - On success, a JSON dictionary will be returned with a single `accounts` - key whose value is list of dicts. Each dict represents an account and - currently only contains the single key `name`. For example:: - - {"accounts": [{"name": "reseller"}, {"name": "test"}, - {"name": "test2"}]} - - :param req: The swob.Request to process. - :returns: swob.Response, 2xx on success with a JSON dictionary as - explained above. - """ - if not self.is_reseller_admin(req): - return self.denied_response(req) - listing = [] - marker = '' - while True: - path = '/v1/%s?format=json&marker=%s' % (quote(self.auth_account), - quote(marker)) - resp = self.make_pre_authed_request( - req.environ, 'GET', path).get_response(self.app) - if resp.status_int // 100 != 2: - raise Exception('Could not list main auth account: %s %s' % - (path, resp.status)) - sublisting = json.loads(resp.body) - if not sublisting: - break - for container in sublisting: - if container['name'][0] != '.': - listing.append({'name': container['name']}) - marker = sublisting[-1]['name'].encode('utf-8') - return Response(body=json.dumps({'accounts': listing}), - content_type=CONTENT_TYPE_JSON) - - def handle_get_account(self, req): - """Handles the GET v2/ call for getting account information. - Can only be called by an account .admin. - - On success, a JSON dictionary will be returned containing the keys - `account_id`, `services`, and `users`. The `account_id` is the value - used when creating service accounts. The `services` value is a dict as - described in the :func:`handle_get_token` call. The `users` value is a - list of dicts, each dict representing a user and currently only - containing the single key `name`. For example:: - - {"account_id": "AUTH_018c3946-23f8-4efb-a8fb-b67aae8e4162", - "services": {"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_018c3946"}}, - "users": [{"name": "tester"}, {"name": "tester3"}]} - - :param req: The swob.Request to process. - :returns: swob.Response, 2xx on success with a JSON dictionary as - explained above. - """ - account = req.path_info_pop() - if req.path_info or not account or account[0] == '.': - return HTTPBadRequest(request=req) - if not self.is_account_admin(req, account): - return self.denied_response(req) - path = quote('/v1/%s/%s/.services' % (self.auth_account, account)) - resp = self.make_pre_authed_request( - req.environ, 'GET', path).get_response(self.app) - if resp.status_int == 404: - return HTTPNotFound(request=req) - if resp.status_int // 100 != 2: - raise Exception('Could not obtain the .services object: %s %s' % - (path, resp.status)) - services = json.loads(resp.body) - listing = [] - marker = '' - while True: - path = '/v1/%s?format=json&marker=%s' % (quote('%s/%s' % - (self.auth_account, account)), quote(marker)) - resp = self.make_pre_authed_request( - req.environ, 'GET', path).get_response(self.app) - if resp.status_int == 404: - return HTTPNotFound(request=req) - if resp.status_int // 100 != 2: - raise Exception('Could not list in main auth account: %s %s' % - (path, resp.status)) - account_id = resp.headers['X-Container-Meta-Account-Id'] - sublisting = json.loads(resp.body) - if not sublisting: - break - for obj in sublisting: - if obj['name'][0] != '.': - listing.append({'name': obj['name']}) - marker = sublisting[-1]['name'].encode('utf-8') - return Response(content_type=CONTENT_TYPE_JSON, - body=json.dumps({'account_id': account_id, - 'services': services, - 'users': listing})) - - def handle_set_services(self, req): - """Handles the POST v2//.services call for setting services - information. Can only be called by a reseller .admin. - - In the :func:`handle_get_account` (GET v2/) call, a section of - the returned JSON dict is `services`. This section looks something like - this:: - - "services": {"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_018c3946"}} - - Making use of this section is described in :func:`handle_get_token`. - - This function allows setting values within this section for the - , allowing the addition of new service end points or updating - existing ones. - - The body of the POST request should contain a JSON dict with the - following format:: - - {"service_name": {"end_point_name": "end_point_value"}} - - There can be multiple services and multiple end points in the same - call. - - Any new services or end points will be added to the existing set of - services and end points. Any existing services with the same service - name will be merged with the new end points. Any existing end points - with the same end point name will have their values updated. - - The updated services dictionary will be returned on success. - - :param req: The swob.Request to process. - :returns: swob.Response, 2xx on success with the udpated services JSON - dict as described above - """ - if not self.is_reseller_admin(req): - return self.denied_response(req) - account = req.path_info_pop() - if req.path_info != '/.services' or not account or account[0] == '.': - return HTTPBadRequest(request=req) - try: - new_services = json.loads(req.body) - except ValueError as err: - return HTTPBadRequest(body=str(err)) - # Get the current services information - path = quote('/v1/%s/%s/.services' % (self.auth_account, account)) - resp = self.make_pre_authed_request( - req.environ, 'GET', path).get_response(self.app) - if resp.status_int == 404: - return HTTPNotFound(request=req) - if resp.status_int // 100 != 2: - raise Exception('Could not obtain services info: %s %s' % - (path, resp.status)) - services = json.loads(resp.body) - for new_service, value in new_services.iteritems(): - if new_service in services: - services[new_service].update(value) - else: - services[new_service] = value - # Save the new services information - services = json.dumps(services) - resp = self.make_pre_authed_request( - req.environ, 'PUT', path, services).get_response(self.app) - if resp.status_int // 100 != 2: - raise Exception('Could not save .services object: %s %s' % - (path, resp.status)) - return Response(request=req, body=services, - content_type=CONTENT_TYPE_JSON) - - def handle_put_account(self, req): - """Handles the PUT v2/ call for adding an account to the auth - system. Can only be called by a .reseller_admin. - - By default, a newly created UUID4 will be used with the reseller prefix - as the account id used when creating corresponding service accounts. - However, you can provide an X-Account-Suffix header to replace the - UUID4 part. - - :param req: The swob.Request to process. - :returns: swob.Response, 2xx on success. - """ - if not self.is_reseller_admin(req): - return self.denied_response(req) - account = req.path_info_pop() - if req.path_info or not account or account[0] == '.': - return HTTPBadRequest(request=req) - - account_suffix = req.headers.get('x-account-suffix') - if not account_suffix: - account_suffix = str(uuid4()) - # Create the new account in the Swift cluster - path = quote('%s/%s%s' % (self.dsc_parsed2.path, - self.reseller_prefix, account_suffix)) - try: - conn = self.get_conn() - conn.request('PUT', path, - headers={'X-Auth-Token': self.get_itoken(req.environ), - 'Content-Length': '0'}) - resp = conn.getresponse() - resp.read() - if resp.status // 100 != 2: - raise Exception('Could not create account on the Swift ' - 'cluster: %s %s %s' % (path, resp.status, resp.reason)) - except (Exception, TimeoutError): - self.logger.error(_('ERROR: Exception while trying to communicate ' - 'with %(scheme)s://%(host)s:%(port)s/%(path)s'), - {'scheme': self.dsc_parsed2.scheme, - 'host': self.dsc_parsed2.hostname, - 'port': self.dsc_parsed2.port, 'path': path}) - raise - # Ensure the container in the main auth account exists (this - # container represents the new account) - path = quote('/v1/%s/%s' % (self.auth_account, account)) - resp = self.make_pre_authed_request( - req.environ, 'HEAD', path).get_response(self.app) - if resp.status_int == 404: - resp = self.make_pre_authed_request( - req.environ, 'PUT', path).get_response(self.app) - if resp.status_int // 100 != 2: - raise Exception('Could not create account within main auth ' - 'account: %s %s' % (path, resp.status)) - elif resp.status_int // 100 == 2: - if 'x-container-meta-account-id' in resp.headers: - # Account was already created - return HTTPAccepted(request=req) - else: - raise Exception('Could not verify account within main auth ' - 'account: %s %s' % (path, resp.status)) - # Record the mapping from account id back to account name - path = quote('/v1/%s/.account_id/%s%s' % - (self.auth_account, self.reseller_prefix, account_suffix)) - resp = self.make_pre_authed_request( - req.environ, 'PUT', path, account).get_response(self.app) - if resp.status_int // 100 != 2: - raise Exception('Could not create account id mapping: %s %s' % - (path, resp.status)) - # Record the cluster url(s) for the account - path = quote('/v1/%s/%s/.services' % (self.auth_account, account)) - services = {'storage': {}} - services['storage'][self.dsc_name] = '%s/%s%s' % (self.dsc_url, - self.reseller_prefix, account_suffix) - services['storage']['default'] = self.dsc_name - resp = self.make_pre_authed_request( - req.environ, 'PUT', path, - json.dumps(services)).get_response(self.app) - if resp.status_int // 100 != 2: - raise Exception('Could not create .services object: %s %s' % - (path, resp.status)) - # Record the mapping from account name to the account id - path = quote('/v1/%s/%s' % (self.auth_account, account)) - resp = self.make_pre_authed_request( - req.environ, 'POST', path, - headers={'X-Container-Meta-Account-Id': '%s%s' % ( - self.reseller_prefix, account_suffix)}).get_response(self.app) - if resp.status_int // 100 != 2: - raise Exception('Could not record the account id on the account: ' - '%s %s' % (path, resp.status)) - return HTTPCreated(request=req) - - def handle_delete_account(self, req): - """Handles the DELETE v2/ call for removing an account from the - auth system. Can only be called by a .reseller_admin. - - :param req: The swob.Request to process. - :returns: swob.Response, 2xx on success. - """ - if not self.is_reseller_admin(req): - return self.denied_response(req) - account = req.path_info_pop() - if req.path_info or not account or account[0] == '.': - return HTTPBadRequest(request=req) - # Make sure the account has no users and get the account_id - marker = '' - while True: - path = '/v1/%s?format=json&marker=%s' % (quote('%s/%s' % - (self.auth_account, account)), quote(marker)) - resp = self.make_pre_authed_request( - req.environ, 'GET', path).get_response(self.app) - if resp.status_int == 404: - return HTTPNotFound(request=req) - if resp.status_int // 100 != 2: - raise Exception('Could not list in main auth account: %s %s' % - (path, resp.status)) - account_id = resp.headers['x-container-meta-account-id'] - sublisting = json.loads(resp.body) - if not sublisting: - break - for obj in sublisting: - if obj['name'][0] != '.': - return HTTPConflict(request=req) - marker = sublisting[-1]['name'].encode('utf-8') - # Obtain the listing of services the account is on. - path = quote('/v1/%s/%s/.services' % (self.auth_account, account)) - resp = self.make_pre_authed_request( - req.environ, 'GET', path).get_response(self.app) - if resp.status_int // 100 != 2 and resp.status_int != 404: - raise Exception('Could not obtain .services object: %s %s' % - (path, resp.status)) - if resp.status_int // 100 == 2: - services = json.loads(resp.body) - # Delete the account on each cluster it is on. - deleted_any = False - for name, url in services['storage'].iteritems(): - if name != 'default': - parsed = urlparse(url) - conn = self.get_conn(parsed) - conn.request('DELETE', parsed.path, - headers={'X-Auth-Token': self.get_itoken(req.environ)}) - resp = conn.getresponse() - resp.read() - if resp.status == 409: - if deleted_any: - raise Exception('Managed to delete one or more ' - 'service end points, but failed with: ' - '%s %s %s' % (url, resp.status, resp.reason)) - else: - return HTTPConflict(request=req) - if resp.status // 100 != 2 and resp.status != 404: - raise Exception('Could not delete account on the ' - 'Swift cluster: %s %s %s' % - (url, resp.status, resp.reason)) - deleted_any = True - # Delete the .services object itself. - path = quote('/v1/%s/%s/.services' % - (self.auth_account, account)) - resp = self.make_pre_authed_request( - req.environ, 'DELETE', path).get_response(self.app) - if resp.status_int // 100 != 2 and resp.status_int != 404: - raise Exception('Could not delete .services object: %s %s' % - (path, resp.status)) - # Delete the account id mapping for the account. - path = quote('/v1/%s/.account_id/%s' % - (self.auth_account, account_id)) - resp = self.make_pre_authed_request( - req.environ, 'DELETE', path).get_response(self.app) - if resp.status_int // 100 != 2 and resp.status_int != 404: - raise Exception('Could not delete account id mapping: %s %s' % - (path, resp.status)) - # Delete the account marker itself. - path = quote('/v1/%s/%s' % (self.auth_account, account)) - resp = self.make_pre_authed_request( - req.environ, 'DELETE', path).get_response(self.app) - if resp.status_int // 100 != 2 and resp.status_int != 404: - raise Exception('Could not delete account marked: %s %s' % - (path, resp.status)) - return HTTPNoContent(request=req) - - def handle_get_user(self, req): - """Handles the GET v2// call for getting user information. - Can only be called by an account .admin. - - On success, a JSON dict will be returned as described:: - - {"groups": [ # List of groups the user is a member of - {"name": ":"}, - # The first group is a unique user identifier - {"name": ""}, - # The second group is the auth account name - {"name": ""} - # There may be additional groups, .admin being a special - # group indicating an account admin and .reseller_admin - # indicating a reseller admin. - ], - "auth": "plaintext:" - # The auth-type and key for the user; currently only plaintext is - # implemented. - } - - For example:: - - {"groups": [{"name": "test:tester"}, {"name": "test"}, - {"name": ".admin"}], - "auth": "plaintext:testing"} - - If the in the request is the special user `.groups`, the JSON - dict will contain a single key of `groups` whose value is a list of - dicts representing the active groups within the account. Each dict - currently has the single key `name`. For example:: - - {"groups": [{"name": ".admin"}, {"name": "test"}, - {"name": "test:tester"}, {"name": "test:tester3"}]} - - :param req: The swob.Request to process. - :returns: swob.Response, 2xx on success with a JSON dictionary as - explained above. - """ - account = req.path_info_pop() - user = req.path_info_pop() - if req.path_info or not account or account[0] == '.' or not user or \ - (user[0] == '.' and user != '.groups'): - return HTTPBadRequest(request=req) - if not self.is_account_admin(req, account): - return self.denied_response(req) - - # get information for each user for the specified - # account and create a list of all groups that the users - # are part of - if user == '.groups': - # TODO(gholt): This could be very slow for accounts with a really - # large number of users. Speed could be improved by concurrently - # requesting user group information. Then again, I don't *know* - # it's slow for `normal` use cases, so testing should be done. - groups = set() - marker = '' - while True: - path = '/v1/%s?format=json&marker=%s' % (quote('%s/%s' % - (self.auth_account, account)), quote(marker)) - resp = self.make_pre_authed_request( - req.environ, 'GET', path).get_response(self.app) - if resp.status_int == 404: - return HTTPNotFound(request=req) - if resp.status_int // 100 != 2: - raise Exception('Could not list in main auth account: ' - '%s %s' % (path, resp.status)) - sublisting = json.loads(resp.body) - if not sublisting: - break - for obj in sublisting: - if obj['name'][0] != '.': - - # get list of groups for each user - user_json = self.get_user_detail(req, account, - obj['name']) - if user_json is None: - raise Exception('Could not retrieve user object: ' - '%s:%s %s' % (account, user, 404)) - groups.update( - g['name'] for g in json.loads(user_json)['groups']) - marker = sublisting[-1]['name'].encode('utf-8') - body = json.dumps( - {'groups': [{'name': g} for g in sorted(groups)]}) - else: - # get information for specific user, - # if user doesn't exist, return HTTPNotFound - body = self.get_user_detail(req, account, user) - if body is None: - return HTTPNotFound(request=req) - - display_groups = [g['name'] for g in json.loads(body)['groups']] - if ('.admin' in display_groups and - not self.is_reseller_admin(req)) or \ - ('.reseller_admin' in display_groups and - not self.is_super_admin(req)): - return self.denied_response(req) - return Response(body=body, content_type=CONTENT_TYPE_JSON) - - def handle_put_user(self, req): - """Handles the PUT v2// call for adding a user to an - account. - - X-Auth-User-Key represents the user's key (url encoded), - - OR - - X-Auth-User-Key-Hash represents the user's hashed key (url encoded), - X-Auth-User-Admin may be set to `true` to create an account .admin, and - X-Auth-User-Reseller-Admin may be set to `true` to create a - .reseller_admin. - - Creating users - ************** - Can only be called by an account .admin unless the user is to be a - .reseller_admin, in which case the request must be by .super_admin. - - Changing password/key - ********************* - 1) reseller_admin key can be changed by super_admin and by himself. - 2) admin key can be changed by any admin in same account, - reseller_admin, super_admin and himself. - 3) Regular user key can be changed by any admin in his account, - reseller_admin, super_admin and himself. - - :param req: The swob.Request to process. - :returns: swob.Response, 2xx on success. - """ - # Validate path info - account = req.path_info_pop() - user = req.path_info_pop() - key = unquote(req.headers.get('x-auth-user-key', '')) - key_hash = unquote(req.headers.get('x-auth-user-key-hash', '')) - admin = req.headers.get('x-auth-user-admin') == 'true' - reseller_admin = \ - req.headers.get('x-auth-user-reseller-admin') == 'true' - if reseller_admin: - admin = True - if req.path_info or not account or account[0] == '.' or not user or \ - user[0] == '.' or (not key and not key_hash): - return HTTPBadRequest(request=req) - if key_hash: - try: - swauth.authtypes.validate_creds(key_hash) - except ValueError: - return HTTPBadRequest(request=req) - - user_arg = account + ':' + user - if reseller_admin: - if not self.is_super_admin(req) and\ - not self.is_user_changing_own_key(req, user_arg): - return self.denied_response(req) - elif not self.is_account_admin(req, account) and\ - not self.is_user_changing_own_key(req, user_arg): - return self.denied_response(req) - - path = quote('/v1/%s/%s' % (self.auth_account, account)) - resp = self.make_pre_authed_request( - req.environ, 'HEAD', path).get_response(self.app) - if resp.status_int // 100 != 2: - raise Exception('Could not retrieve account id value: %s %s' % - (path, resp.status)) - headers = {'X-Object-Meta-Account-Id': - resp.headers['x-container-meta-account-id']} - # Create the object in the main auth account (this object represents - # the user) - path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user)) - groups = ['%s:%s' % (account, user), account] - if admin: - groups.append('.admin') - if reseller_admin: - groups.append('.reseller_admin') - auth_value = key_hash or self.auth_encoder().encode(key) - resp = self.make_pre_authed_request( - req.environ, 'PUT', path, - json.dumps({'auth': auth_value, - 'groups': [{'name': g} for g in groups]}), - headers=headers).get_response(self.app) - if resp.status_int == 404: - return HTTPNotFound(request=req) - if resp.status_int // 100 != 2: - raise Exception('Could not create user object: %s %s' % - (path, resp.status)) - return HTTPCreated(request=req) - - def handle_delete_user(self, req): - """Handles the DELETE v2// call for deleting a user from an - account. - - Can only be called by an account .admin. - - :param req: The swob.Request to process. - :returns: swob.Response, 2xx on success. - """ - # Validate path info - account = req.path_info_pop() - user = req.path_info_pop() - if req.path_info or not account or account[0] == '.' or not user or \ - user[0] == '.': - return HTTPBadRequest(request=req) - - # if user to be deleted is reseller_admin, then requesting - # user must be the super_admin - is_reseller_admin = self.is_user_reseller_admin(req, account, user) - if not is_reseller_admin and not req.credentials_valid: - # if user to be deleted can't be found, return 404 - return HTTPNotFound(request=req) - elif is_reseller_admin and not self.is_super_admin(req): - return HTTPForbidden(request=req) - - if not self.is_account_admin(req, account): - return self.denied_response(req) - - # Delete the user's existing token, if any. - path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user)) - resp = self.make_pre_authed_request( - req.environ, 'HEAD', path).get_response(self.app) - if resp.status_int == 404: - return HTTPNotFound(request=req) - elif resp.status_int // 100 != 2: - raise Exception('Could not obtain user details: %s %s' % - (path, resp.status)) - candidate_token = resp.headers.get('x-object-meta-auth-token') - if candidate_token: - object_name = self._get_concealed_token(candidate_token) - path = quote('/v1/%s/.token_%s/%s' % - (self.auth_account, object_name[-1], object_name)) - resp = self.make_pre_authed_request( - req.environ, 'DELETE', path).get_response(self.app) - if resp.status_int // 100 != 2 and resp.status_int != 404: - raise Exception('Could not delete possibly existing token: ' - '%s %s' % (path, resp.status)) - # Delete the user entry itself. - path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user)) - resp = self.make_pre_authed_request( - req.environ, 'DELETE', path).get_response(self.app) - if resp.status_int // 100 != 2 and resp.status_int != 404: - raise Exception('Could not delete the user object: %s %s' % - (path, resp.status)) - return HTTPNoContent(request=req) - - def is_user_reseller_admin(self, req, account, user): - """Returns True if the user is a .reseller_admin. - - :param account: account user is part of - :param user: the user - :returns: True if user .reseller_admin, False - if user is not a reseller_admin and None if the user - doesn't exist. - """ - req.credentials_valid = True - user_json = self.get_user_detail(req, account, user) - if user_json is None: - req.credentials_valid = False - return False - - user_detail = json.loads(user_json) - - return '.reseller_admin' in (g['name'] for g in user_detail['groups']) - - def handle_get_token(self, req): - """Handles the various `request for token and service end point(s)` calls. - There are various formats to support the various auth servers in the - past. Examples:: - - GET /v1//auth - X-Auth-User: : or X-Storage-User: - X-Auth-Key: or X-Storage-Pass: - GET /auth - X-Auth-User: : or X-Storage-User: : - X-Auth-Key: or X-Storage-Pass: - GET /v1.0 - X-Auth-User: : or X-Storage-User: : - X-Auth-Key: or X-Storage-Pass: - - Values should be url encoded, "act%3Ausr" instead of "act:usr" for - example; however, for backwards compatibility the colon may be included - unencoded. - - On successful authentication, the response will have X-Auth-Token and - X-Storage-Token set to the token to use with Swift and X-Storage-URL - set to the URL to the default Swift cluster to use. - - The response body will be set to the account's services JSON object as - described here:: - - {"storage": { # Represents the Swift storage service end points - "default": "cluster1", # Indicates which cluster is the default - "cluster1": "", - # A Swift cluster that can be used with this account, - # "cluster1" is the name of the cluster which is usually a - # location indicator (like "dfw" for a datacenter region). - "cluster2": "" - # Another Swift cluster that can be used with this account, - # there will always be at least one Swift cluster to use or - # this whole "storage" dict won't be included at all. - }, - "servers": { # Represents the Nova server service end points - # Expected to be similar to the "storage" dict, but not - # implemented yet. - }, - # Possibly other service dicts, not implemented yet. - } - - One can also include an "X-Auth-New-Token: true" header to - force issuing a new token and revoking any old token, even if - it hasn't expired yet. - - :param req: The swob.Request to process. - :returns: swob.Response, 2xx on success with data set as explained - above. - """ - # Validate the request info - try: - pathsegs = split_path(req.path_info, minsegs=1, maxsegs=3, - rest_with_last=True) - except ValueError: - return HTTPNotFound(request=req) - if pathsegs[0] == 'v1' and pathsegs[2] == 'auth': - account = pathsegs[1] - user = req.headers.get('x-storage-user') - if not user: - user = unquote(req.headers.get('x-auth-user', '')) - if not user or ':' not in user: - return HTTPUnauthorized(request=req) - account2, user = user.split(':', 1) - if account != account2: - return HTTPUnauthorized(request=req) - key = req.headers.get('x-storage-pass') - if not key: - key = unquote(req.headers.get('x-auth-key', '')) - elif pathsegs[0] in ('auth', 'v1.0'): - user = unquote(req.headers.get('x-auth-user', '')) - if not user: - user = req.headers.get('x-storage-user') - if not user or ':' not in user: - return HTTPUnauthorized(request=req) - account, user = user.split(':', 1) - key = unquote(req.headers.get('x-auth-key', '')) - if not key: - key = req.headers.get('x-storage-pass') - else: - return HTTPBadRequest(request=req) - if not all((account, user, key)): - return HTTPUnauthorized(request=req) - if user == '.super_admin' and self.super_admin_key and \ - key == self.super_admin_key: - token = self.get_itoken(req.environ) - url = '%s/%s.auth' % (self.dsc_url, self.reseller_prefix) - return Response( - request=req, - content_type=CONTENT_TYPE_JSON, - body=json.dumps({'storage': {'default': 'local', - 'local': url}}), - headers={'x-auth-token': token, - 'x-storage-token': token, - 'x-storage-url': url}) - - # Authenticate user - path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user)) - resp = self.make_pre_authed_request( - req.environ, 'GET', path).get_response(self.app) - if resp.status_int == 404: - return HTTPUnauthorized(request=req) - if resp.status_int // 100 != 2: - raise Exception('Could not obtain user details: %s %s' % - (path, resp.status)) - user_detail = json.loads(resp.body) - if not self.credentials_match(user_detail, key): - return HTTPUnauthorized(request=req) - # See if a token already exists and hasn't expired - token = None - expires = None - candidate_token = resp.headers.get('x-object-meta-auth-token') - if candidate_token: - object_name = self._get_concealed_token(candidate_token) - path = quote('/v1/%s/.token_%s/%s' % - (self.auth_account, object_name[-1], object_name)) - delete_token = False - try: - if req.headers.get('x-auth-new-token', 'false').lower() in \ - TRUE_VALUES: - delete_token = True - else: - resp = self.make_pre_authed_request( - req.environ, 'GET', path).get_response(self.app) - if resp.status_int // 100 == 2: - token_detail = json.loads(resp.body) - if token_detail['expires'] > time(): - token = candidate_token - expires = token_detail['expires'] - else: - delete_token = True - elif resp.status_int != 404: - raise Exception( - 'Could not detect whether a token already exists: ' - '%s %s' % (path, resp.status)) - finally: - if delete_token: - self.make_pre_authed_request( - req.environ, 'DELETE', path).get_response(self.app) - memcache_client = cache_from_env(req.environ) - if memcache_client: - memcache_key = '%s/auth/%s' % (self.reseller_prefix, - candidate_token) - memcache_client.delete(memcache_key) - # Create a new token if one didn't exist - if not token: - # Retrieve account id, we'll save this in the token - path = quote('/v1/%s/%s' % (self.auth_account, account)) - resp = self.make_pre_authed_request( - req.environ, 'HEAD', path).get_response(self.app) - if resp.status_int // 100 != 2: - raise Exception('Could not retrieve account id value: ' - '%s %s' % (path, resp.status)) - account_id = \ - resp.headers['x-container-meta-account-id'] - # Generate new token - token = '%stk%s' % (self.reseller_prefix, uuid4().hex) - # Save token info - object_name = self._get_concealed_token(token) - path = quote('/v1/%s/.token_%s/%s' % - (self.auth_account, object_name[-1], object_name)) - try: - token_life = min( - int(req.headers.get('x-auth-token-lifetime', - self.token_life)), - self.max_token_life) - except ValueError: - token_life = self.token_life - expires = int(time() + token_life) - resp = self.make_pre_authed_request( - req.environ, 'PUT', path, - json.dumps({'account': account, 'user': user, - 'account_id': account_id, - 'groups': user_detail['groups'], - 'expires': expires})).get_response(self.app) - if resp.status_int // 100 != 2: - raise Exception('Could not create new token: %s %s' % - (path, resp.status)) - # Record the token with the user info for future use. - path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user)) - resp = self.make_pre_authed_request( - req.environ, 'POST', path, - headers={'X-Object-Meta-Auth-Token': token} - ).get_response(self.app) - if resp.status_int // 100 != 2: - raise Exception('Could not save new token: %s %s' % - (path, resp.status)) - # Get the services information - path = quote('/v1/%s/%s/.services' % (self.auth_account, account)) - resp = self.make_pre_authed_request( - req.environ, 'GET', path).get_response(self.app) - if resp.status_int // 100 != 2: - raise Exception('Could not obtain services info: %s %s' % - (path, resp.status)) - detail = json.loads(resp.body) - url = detail['storage'][detail['storage']['default']] - return Response( - request=req, - body=resp.body, - content_type=CONTENT_TYPE_JSON, - headers={'x-auth-token': token, - 'x-storage-token': token, - 'x-auth-token-expires': str(int(expires - time())), - 'x-storage-url': url}) - - def handle_validate_token(self, req): - """Handles the GET v2/.token/ call for validating a token, usually - called by a service like Swift. - - On a successful validation, X-Auth-TTL will be set for how much longer - this token is valid and X-Auth-Groups will contain a comma separated - list of groups the user belongs to. - - The first group listed will be a unique identifier for the user the - token represents. - - .reseller_admin is a special group that indicates the user should be - allowed to do anything on any account. - - :param req: The swob.Request to process. - :returns: swob.Response, 2xx on success with data set as explained - above. - """ - token = req.path_info_pop() - if req.path_info or not token.startswith(self.reseller_prefix): - return HTTPBadRequest(request=req) - expires = groups = None - memcache_client = cache_from_env(req.environ) - if memcache_client: - memcache_key = '%s/auth/%s' % (self.reseller_prefix, token) - cached_auth_data = memcache_client.get(memcache_key) - if cached_auth_data: - expires, groups = cached_auth_data - if expires < time(): - groups = None - if not groups: - object_name = self._get_concealed_token(token) - path = quote('/v1/%s/.token_%s/%s' % - (self.auth_account, object_name[-1], object_name)) - resp = self.make_pre_authed_request( - req.environ, 'GET', path).get_response(self.app) - if resp.status_int // 100 != 2: - return HTTPNotFound(request=req) - detail = json.loads(resp.body) - expires = detail['expires'] - if expires < time(): - self.make_pre_authed_request( - req.environ, 'DELETE', path).get_response(self.app) - return HTTPNotFound(request=req) - groups = [g['name'] for g in detail['groups']] - if '.admin' in groups: - groups.remove('.admin') - groups.append(detail['account_id']) - groups = ','.join(groups) - return HTTPNoContent(headers={'X-Auth-TTL': expires - time(), - 'X-Auth-Groups': groups}) - - def get_conn(self, urlparsed=None): - """Returns an HTTPConnection based on the urlparse result given or the - default Swift cluster (internal url) urlparse result. - - :param urlparsed: The result from urlparse.urlparse or None to use the - default Swift cluster's value - """ - if not urlparsed: - urlparsed = self.dsc_parsed2 - if urlparsed.scheme == 'http': - return HTTPConnection(urlparsed.netloc) - else: - return HTTPSConnection(urlparsed.netloc) - - def get_itoken(self, env): - """Returns the current internal token to use for the auth system's own - actions with other services. Each process will create its own - itoken and the token will be deleted and recreated based on the - token_life configuration value. The itoken information is stored in - memcache because the auth process that is asked by Swift to validate - the token may not be the same as the auth process that created the - token. - """ - if not self.itoken or self.itoken_expires < time() or \ - env.get('HTTP_X_AUTH_NEW_TOKEN', 'false').lower() in \ - TRUE_VALUES: - self.itoken = '%sitk%s' % (self.reseller_prefix, uuid4().hex) - memcache_key = '%s/auth/%s' % (self.reseller_prefix, self.itoken) - self.itoken_expires = time() + self.token_life - memcache_client = cache_from_env(env) - if not memcache_client: - raise Exception( - 'No memcache set up; required for Swauth middleware') - memcache_client.set( - memcache_key, - (self.itoken_expires, - '.auth,.reseller_admin,%s.auth' % self.reseller_prefix), - time=self.token_life) - return self.itoken - - def get_admin_detail(self, req): - """Returns the dict for the user specified as the admin in the request - with the addition of an `account` key set to the admin user's account. - - :param req: The swob request to retrieve X-Auth-Admin-User and - X-Auth-Admin-Key from. - :returns: The dict for the admin user with the addition of the - `account` key. - """ - if ':' not in req.headers.get('x-auth-admin-user', ''): - return None - admin_account, admin_user = \ - req.headers.get('x-auth-admin-user').split(':', 1) - user_json = self.get_user_detail(req, admin_account, admin_user) - if user_json is None: - return None - admin_detail = json.loads(user_json) - admin_detail['account'] = admin_account - return admin_detail - - def get_user_detail(self, req, account, user): - """Returns the response body of a GET request for the specified user - The body is in JSON format and contains all user information. - - :param req: The swob request - :param account: the account the user is a member of - :param user: the user - - :returns: A JSON response with the user detail information, None - if the user doesn't exist - """ - path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user)) - resp = self.make_pre_authed_request( - req.environ, 'GET', path).get_response(self.app) - if resp.status_int == 404: - return None - if resp.status_int // 100 != 2: - raise Exception('Could not get user object: %s %s' % - (path, resp.status)) - return resp.body - - def credentials_match(self, user_detail, key): - """Returns True if the key is valid for the user_detail. - It will use auth_encoder type the password was encoded with, - to check for a key match. - - :param user_detail: The dict for the user. - :param key: The key to validate for the user. - :returns: True if the key is valid for the user, False if not. - """ - if user_detail: - creds = user_detail.get('auth') - try: - auth_encoder, creds_dict = \ - swauth.authtypes.validate_creds(creds) - except ValueError as e: - self.logger.error('%s' % e.args[0]) - return False - return user_detail and auth_encoder.match(key, creds, **creds_dict) - - def is_user_changing_own_key(self, req, user): - """Check if the user is changing his own key. - - :param req: The swob.Request to check. This contains x-auth-admin-user - and x-auth-admin-key headers which are credentials of the - user sending the request. - :param user: User whose password is to be changed. - :returns: True if user is changing his own key, False if not. - """ - admin_detail = self.get_admin_detail(req) - if not admin_detail: - # The user does not exist - return False - - # If user is not admin/reseller_admin and x-auth-user-admin or - # x-auth-user-reseller-admin headers are present in request, he may be - # attempting to escalate himself as admin/reseller_admin! - if '.admin' not in (g['name'] for g in admin_detail['groups']): - if req.headers.get('x-auth-user-admin') == 'true' or \ - req.headers.get('x-auth-user-reseller-admin') == 'true': - return False - if '.reseller_admin' not in \ - (g['name'] for g in admin_detail['groups']) and \ - req.headers.get('x-auth-user-reseller-admin') == 'true': - return False - - return req.headers.get('x-auth-admin-user') == user and \ - self.credentials_match(admin_detail, - req.headers.get('x-auth-admin-key')) - - def is_super_admin(self, req): - """Returns True if the admin specified in the request represents the - .super_admin. - - :param req: The swob.Request to check. - :param returns: True if .super_admin. - """ - return req.headers.get('x-auth-admin-user') == '.super_admin' and \ - self.super_admin_key and \ - req.headers.get('x-auth-admin-key') == self.super_admin_key - - def is_reseller_admin(self, req, admin_detail=None): - """Returns True if the admin specified in the request represents a - .reseller_admin. - - :param req: The swob.Request to check. - :param admin_detail: The previously retrieved dict from - :func:`get_admin_detail` or None for this function - to retrieve the admin_detail itself. - :param returns: True if .reseller_admin. - """ - req.credentials_valid = False - if self.is_super_admin(req): - return True - if not admin_detail: - admin_detail = self.get_admin_detail(req) - if not self.credentials_match(admin_detail, - req.headers.get('x-auth-admin-key')): - return False - req.credentials_valid = True - return '.reseller_admin' in (g['name'] for g in admin_detail['groups']) - - def is_account_admin(self, req, account): - """Returns True if the admin specified in the request represents a .admin - for the account specified. - - :param req: The swob.Request to check. - :param account: The account to check for .admin against. - :param returns: True if .admin. - """ - req.credentials_valid = False - if self.is_super_admin(req): - return True - admin_detail = self.get_admin_detail(req) - if admin_detail: - if self.is_reseller_admin(req, admin_detail=admin_detail): - return True - if not self.credentials_match(admin_detail, - req.headers.get('x-auth-admin-key')): - return False - req.credentials_valid = True - return admin_detail and admin_detail['account'] == account and \ - '.admin' in (g['name'] for g in admin_detail['groups']) - return False - - def posthooklogger(self, env, req): - if not req.path.startswith(self.auth_prefix): - return - response = getattr(req, 'response', None) - if not response: - return - trans_time = '%.4f' % (time() - req.start_time) - the_request = quote(unquote(req.path)) - if req.query_string: - the_request = the_request + '?' + req.query_string - # remote user for zeus - client = req.headers.get('x-cluster-client-ip') - if not client and 'x-forwarded-for' in req.headers: - # remote user for other lbs - client = req.headers['x-forwarded-for'].split(',')[0].strip() - logged_headers = None - if self.log_headers: - logged_headers = '\n'.join('%s: %s' % (k, v) - for k, v in req.headers.items()) - status_int = response.status_int - if getattr(req, 'client_disconnect', False) or \ - getattr(response, 'client_disconnect', False): - status_int = 499 - self.logger.info(' '.join(quote(str(x)) for x in (client or '-', - req.remote_addr or '-', strftime('%d/%b/%Y/%H/%M/%S', gmtime()), - req.method, the_request, req.environ['SERVER_PROTOCOL'], - status_int, req.referer or '-', req.user_agent or '-', - req.headers.get('x-auth-token', - req.headers.get('x-auth-admin-user', '-')), - getattr(req, 'bytes_transferred', 0) or '-', - getattr(response, 'bytes_transferred', 0) or '-', - req.headers.get('etag', '-'), - req.headers.get('x-trans-id', '-'), logged_headers or '-', - trans_time))) - - -def filter_factory(global_conf, **local_conf): - """Returns a WSGI filter app for use with paste.deploy.""" - conf = global_conf.copy() - conf.update(local_conf) - - def auth_filter(app): - return Swauth(app, conf) - return auth_filter diff --git a/swauth/swift_version.py b/swauth/swift_version.py deleted file mode 100644 index dd96f97..0000000 --- a/swauth/swift_version.py +++ /dev/null @@ -1,82 +0,0 @@ -# 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 swift - - -MAJOR = None -MINOR = None -REVISION = None -FINAL = None - - -def parse(value): - parts = value.split('.') - if parts[-1].endswith('-dev'): - final = False - parts[-1] = parts[-1][:-4] - else: - final = True - major = int(parts.pop(0)) - minor = int(parts.pop(0)) - if parts: - revision = int(parts.pop(0).split('-', 1)[0]) - else: - revision = 0 - return major, minor, revision, final - - -def newer_than(value): - global MAJOR, MINOR, REVISION, FINAL - try: - major, minor, revision, final = parse(value) - if MAJOR is None: - MAJOR, MINOR, REVISION, FINAL = parse(swift.__version__) - if MAJOR < major: - return False - elif MAJOR == major: - if MINOR < minor: - return False - elif MINOR == minor: - if REVISION < revision: - return False - elif REVISION == revision: - if not FINAL or final: - return False - except Exception: - # Unable to detect if it's newer, better to fail - return False - return True - - -def at_least(value): - global MAJOR, MINOR, REVISION, FINAL - try: - major, minor, revision, final = parse(value) - if MAJOR is None: - MAJOR, MINOR, REVISION, FINAL = parse(swift.__version__) - if MAJOR < major: - return False - elif MAJOR == major: - if MINOR < minor: - return False - elif MINOR == minor: - if REVISION < revision: - return False - elif REVISION == revision: - if not FINAL and final: - return False - except Exception: - # Unable to detect if it's newer, better to fail - return False - return True diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index b831318..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. -hacking<0.11,>=0.10.0 - -flake8<2.6.0,>=2.5.4 # MIT -mock>=2.0.0 # BSD -nose>=1.3.7 # LGPL -coverage!=4.4,>=4.0 # Apache-2.0 -#discover -#python-subunit>=0.0.18 -sphinx>=1.6.2 # BSD -bandit>=1.1.0 # Apache-2.0 diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index a940b8c..0000000 --- a/test/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# See http://code.google.com/p/python-nose/issues/detail?id=373 -# The code below enables nosetests to work with i18n _() blocks - -import six.moves.builtins as __builtin__ - -setattr(__builtin__, '_', lambda x: x) diff --git a/test/unit/__init__.py b/test/unit/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/unit/test_authtypes.py b/test/unit/test_authtypes.py deleted file mode 100644 index 669af90..0000000 --- a/test/unit/test_authtypes.py +++ /dev/null @@ -1,207 +0,0 @@ -# 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. -# -# Pablo Llopis 2011 - -import mock -from swauth import authtypes -import unittest - - -class TestValidation(unittest.TestCase): - def test_validate_creds(self): - creds = 'plaintext:keystring' - creds_dict = dict(type='plaintext', salt=None, hash='keystring') - auth_encoder, parsed_creds = authtypes.validate_creds(creds) - self.assertEqual(parsed_creds, creds_dict) - self.assertTrue(isinstance(auth_encoder, authtypes.Plaintext)) - - creds = 'sha1:salt$d50dc700c296e23ce5b41f7431a0e01f69010f06' - creds_dict = dict(type='sha1', salt='salt', - hash='d50dc700c296e23ce5b41f7431a0e01f69010f06') - auth_encoder, parsed_creds = authtypes.validate_creds(creds) - self.assertEqual(parsed_creds, creds_dict) - self.assertTrue(isinstance(auth_encoder, authtypes.Sha1)) - - creds = ('sha512:salt$482e73705fac6909e2d78e8bbaf65ac3ca1473' - '8f445cc2367b7daa3f0e8f3dcfe798e426b9e332776c8da59c' - '0c11d4832931d1bf48830f670ecc6ceb04fbad0f') - creds_dict = dict(type='sha512', salt='salt', - hash='482e73705fac6909e2d78e8bbaf65ac3ca1473' - '8f445cc2367b7daa3f0e8f3dcfe798e426b9e3' - '32776c8da59c0c11d4832931d1bf48830f670e' - 'cc6ceb04fbad0f') - auth_encoder, parsed_creds = authtypes.validate_creds(creds) - self.assertEqual(parsed_creds, creds_dict) - self.assertTrue(isinstance(auth_encoder, authtypes.Sha512)) - - def test_validate_creds_fail(self): - # wrong format, missing `:` - creds = 'unknown;keystring' - self.assertRaisesRegexp(ValueError, "Missing ':' in .*", - authtypes.validate_creds, creds) - # unknown auth_type - creds = 'unknown:keystring' - self.assertRaisesRegexp(ValueError, "Invalid auth_type: .*", - authtypes.validate_creds, creds) - # wrong plaintext keystring - creds = 'plaintext:' - self.assertRaisesRegexp(ValueError, "Key must have non-zero length!", - authtypes.validate_creds, creds) - # wrong sha1 format, missing `$` - creds = 'sha1:saltkeystring' - self.assertRaisesRegexp(ValueError, "Missing '\$' in .*", - authtypes.validate_creds, creds) - # wrong sha1 format, missing salt - creds = 'sha1:$hash' - self.assertRaisesRegexp(ValueError, "Salt must have non-zero length!", - authtypes.validate_creds, creds) - # wrong sha1 format, missing hash - creds = 'sha1:salt$' - self.assertRaisesRegexp(ValueError, "Hash must have 40 chars!", - authtypes.validate_creds, creds) - # wrong sha1 format, short hash - creds = 'sha1:salt$short_hash' - self.assertRaisesRegexp(ValueError, "Hash must have 40 chars!", - authtypes.validate_creds, creds) - # wrong sha1 format, wrong format - creds = 'sha1:salt$' + "z" * 40 - self.assertRaisesRegexp(ValueError, "Hash must be hexadecimal!", - authtypes.validate_creds, creds) - # wrong sha512 format, missing `$` - creds = 'sha512:saltkeystring' - self.assertRaisesRegexp(ValueError, "Missing '\$' in .*", - authtypes.validate_creds, creds) - # wrong sha512 format, missing salt - creds = 'sha512:$hash' - self.assertRaisesRegexp(ValueError, "Salt must have non-zero length!", - authtypes.validate_creds, creds) - # wrong sha512 format, missing hash - creds = 'sha512:salt$' - self.assertRaisesRegexp(ValueError, "Hash must have 128 chars!", - authtypes.validate_creds, creds) - # wrong sha512 format, short hash - creds = 'sha512:salt$short_hash' - self.assertRaisesRegexp(ValueError, "Hash must have 128 chars!", - authtypes.validate_creds, creds) - # wrong sha1 format, wrong format - creds = 'sha512:salt$' + "z" * 128 - self.assertRaisesRegexp(ValueError, "Hash must be hexadecimal!", - authtypes.validate_creds, creds) - - -class TestPlaintext(unittest.TestCase): - - def setUp(self): - self.auth_encoder = authtypes.Plaintext() - - def test_plaintext_encode(self): - enc_key = self.auth_encoder.encode('keystring') - self.assertEqual('plaintext:keystring', enc_key) - - def test_plaintext_valid_match(self): - creds = 'plaintext:keystring' - match = self.auth_encoder.match('keystring', creds) - self.assertEqual(match, True) - - def test_plaintext_invalid_match(self): - creds = 'plaintext:other-keystring' - match = self.auth_encoder.match('keystring', creds) - self.assertEqual(match, False) - - -class TestSha1(unittest.TestCase): - - def setUp(self): - self.auth_encoder = authtypes.Sha1() - self.auth_encoder.salt = 'salt' - - @mock.patch('swauth.authtypes.os') - def test_sha1_encode(self, os): - os.urandom.return_value.encode.return_value.rstrip \ - .return_value = 'salt' - enc_key = self.auth_encoder.encode('keystring') - self.assertEqual('sha1:salt$d50dc700c296e23ce5b41f7431a0e01f69010f06', - enc_key) - - def test_sha1_valid_match(self): - creds = 'sha1:salt$d50dc700c296e23ce5b41f7431a0e01f69010f06' - creds_dict = dict(type='sha1', salt='salt', - hash='d50dc700c296e23ce5b41f7431a0e01f69010f06') - match = self.auth_encoder.match('keystring', creds, **creds_dict) - self.assertEqual(match, True) - - def test_sha1_invalid_match(self): - creds = 'sha1:salt$deadbabedeadbabedeadbabec0ffeebadc0ffeee' - creds_dict = dict(type='sha1', salt='salt', - hash='deadbabedeadbabedeadbabec0ffeebadc0ffeee') - match = self.auth_encoder.match('keystring', creds, **creds_dict) - self.assertEqual(match, False) - - creds = 'sha1:salt$d50dc700c296e23ce5b41f7431a0e01f69010f06' - creds_dict = dict(type='sha1', salt='salt', - hash='d50dc700c296e23ce5b41f7431a0e01f69010f06') - match = self.auth_encoder.match('keystring2', creds, **creds_dict) - self.assertEqual(match, False) - - -class TestSha512(unittest.TestCase): - - def setUp(self): - self.auth_encoder = authtypes.Sha512() - self.auth_encoder.salt = 'salt' - - @mock.patch('swauth.authtypes.os') - def test_sha512_encode(self, os): - os.urandom.return_value.encode.return_value.rstrip \ - .return_value = 'salt' - enc_key = self.auth_encoder.encode('keystring') - self.assertEqual('sha512:salt$482e73705fac6909e2d78e8bbaf65ac3ca1473' - '8f445cc2367b7daa3f0e8f3dcfe798e426b9e332776c8da59c' - '0c11d4832931d1bf48830f670ecc6ceb04fbad0f', enc_key) - - def test_sha512_valid_match(self): - creds = ('sha512:salt$482e73705fac6909e2d78e8bbaf65ac3ca14738f445cc2' - '367b7daa3f0e8f3dcfe798e426b9e332776c8da59c0c11d4832931d1bf' - '48830f670ecc6ceb04fbad0f') - creds_dict = dict(type='sha512', salt='salt', - hash='482e73705fac6909e2d78e8bbaf65ac3ca14738f445cc2' - '367b7daa3f0e8f3dcfe798e426b9e332776c8da59c0c11' - 'd4832931d1bf48830f670ecc6ceb04fbad0f') - match = self.auth_encoder.match('keystring', creds, **creds_dict) - self.assertEqual(match, True) - - def test_sha512_invalid_match(self): - creds = ('sha512:salt$deadbabedeadbabedeadbabedeadbabedeadbabedeadba' - 'bedeadbabedeadbabedeadbabedeadbabedeadbabedeadbabedeadbabe' - 'c0ffeebadc0ffeeec0ffeeba') - creds_dict = dict(type='sha512', salt='salt', - hash='deadbabedeadbabedeadbabedeadbabedeadbabedeadba' - 'bedeadbabedeadbabedeadbabedeadbabedeadbabedead' - 'babedeadbabec0ffeebadc0ffeeec0ffeeba') - match = self.auth_encoder.match('keystring', creds, **creds_dict) - self.assertEqual(match, False) - - creds = ('sha512:salt$482e73705fac6909e2d78e8bbaf65ac3ca14738f445cc2' - '367b7daa3f0e8f3dcfe798e426b9e332776c8da59c0c11d4832931d1bf' - '48830f670ecc6ceb04fbad0f') - creds_dict = dict(type='sha512', salt='salt', - hash='482e73705fac6909e2d78e8bbaf65ac3ca14738f445cc2' - '367b7daa3f0e8f3dcfe798e426b9e332776c8da59c0c11' - 'd4832931d1bf48830f670ecc6ceb04fbad0f') - match = self.auth_encoder.match('keystring2', creds, **creds_dict) - self.assertEqual(match, False) - - -if __name__ == '__main__': - unittest.main() diff --git a/test/unit/test_middleware.py b/test/unit/test_middleware.py deleted file mode 100644 index 1d1dd95..0000000 --- a/test/unit/test_middleware.py +++ /dev/null @@ -1,4197 +0,0 @@ -# Copyright (c) 2010-2011 OpenStack, LLC. -# -# 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 base64 -from contextlib import contextmanager -import hashlib -import json -import mock -from time import time -import unittest -from urllib import quote - -from swift.common.swob import Request -from swift.common.swob import Response - -from swauth.authtypes import MAX_TOKEN_LENGTH -from swauth import middleware as auth - - -CONTENT_TYPE_JSON = 'application/json' -DEFAULT_TOKEN_LIFE = 86400 -MAX_TOKEN_LIFE = 100000 - - -class FakeMemcache(object): - - def __init__(self): - self.store = {} - - def get(self, key): - return self.store.get(key) - - def set(self, key, value, time=0): - self.store[key] = value - return True - - def incr(self, key, time=0): - self.store[key] = self.store.setdefault(key, 0) + 1 - return self.store[key] - - @contextmanager - def soft_lock(self, key, retries=5, time=0): - yield True - - def delete(self, key): - try: - del self.store[key] - except Exception: - pass - return True - - -class FakeApp(object): - - def __init__(self, status_headers_body_iter=None, acl=None, sync_key=None): - self.calls = 0 - self.status_headers_body_iter = status_headers_body_iter - if not self.status_headers_body_iter: - self.status_headers_body_iter = iter([('404 Not Found', {}, '')]) - self.acl = acl - self.sync_key = sync_key - - def __call__(self, env, start_response): - self.calls += 1 - self.request = Request.blank('', environ=env) - if self.acl: - self.request.acl = self.acl - if self.sync_key: - self.request.environ['swift_sync_key'] = self.sync_key - if 'swift.authorize' in env: - resp = env['swift.authorize'](self.request) - if resp: - return resp(env, start_response) - status, headers, body = self.status_headers_body_iter.next() - return Response(status=status, headers=headers, - body=body)(env, start_response) - - -class FakeConn(object): - - def __init__(self, status_headers_body_iter=None): - self.calls = 0 - self.status_headers_body_iter = status_headers_body_iter - if not self.status_headers_body_iter: - self.status_headers_body_iter = iter([('404 Not Found', {}, '')]) - - def request(self, method, path, headers): - self.calls += 1 - self.request_path = path - self.status, self.headers, self.body = \ - self.status_headers_body_iter.next() - self.status, self.reason = self.status.split(' ', 1) - self.status = int(self.status) - - def getresponse(self): - return self - - def read(self): - body = self.body - self.body = '' - return body - - -class TestAuth(unittest.TestCase): - - def setUp(self): - self.test_auth = \ - auth.filter_factory({ - 'super_admin_key': 'supertest', - 'token_life': str(DEFAULT_TOKEN_LIFE), - 'max_token_life': str(MAX_TOKEN_LIFE)})(FakeApp()) - - def test_salt(self): - for auth_type in ('sha1', 'sha512'): - # Salt not manually set - test_auth = \ - auth.filter_factory({ - 'super_admin_key': 'supertest', - 'token_life': str(DEFAULT_TOKEN_LIFE), - 'max_token_life': str(MAX_TOKEN_LIFE), - 'auth_type': auth_type})(FakeApp()) - self.assertEqual(test_auth.auth_encoder.salt, None) - mock_urandom = mock.Mock(return_value="abc") - with mock.patch("os.urandom", mock_urandom): - h_key = test_auth.auth_encoder().encode("key") - self.assertTrue(mock_urandom.called) - prefix = auth_type + ":" + "abc".encode('base64').rstrip() + '$' - self.assertTrue(h_key.startswith(prefix)) - - # Salt manually set - test_auth = \ - auth.filter_factory({ - 'super_admin_key': 'supertest', - 'token_life': str(DEFAULT_TOKEN_LIFE), - 'max_token_life': str(MAX_TOKEN_LIFE), - 'auth_type': auth_type, - 'auth_type_salt': "mysalt"})(FakeApp()) - self.assertEqual(test_auth.auth_encoder.salt, "mysalt") - mock_urandom = mock.Mock() - with mock.patch("os.urandom", mock_urandom): - h_key = test_auth.auth_encoder().encode("key") - self.assertFalse(mock_urandom.called) - prefix = auth_type + ":" + "mysalt" + '$' - self.assertTrue(h_key.startswith(prefix)) - - def test_swift_version(self): - app = FakeApp() - - with mock.patch('swauth.swift_version.at_least') as mock_at_least: - mock_at_least.return_value = False - self.assertRaises(ValueError, auth.filter_factory({}), app) - - def test_super_admin_key_not_required(self): - auth.filter_factory({})(FakeApp()) - - def test_reseller_prefix_init(self): - app = FakeApp() - ath = auth.filter_factory({'super_admin_key': 'supertest'})(app) - self.assertEqual(ath.reseller_prefix, 'AUTH_') - ath = auth.filter_factory({'super_admin_key': 'supertest', - 'reseller_prefix': 'TEST'})(app) - self.assertEqual(ath.reseller_prefix, 'TEST_') - ath = auth.filter_factory({'super_admin_key': 'supertest', - 'reseller_prefix': 'TEST_'})(app) - self.assertEqual(ath.reseller_prefix, 'TEST_') - - def test_auth_prefix_init(self): - app = FakeApp() - ath = auth.filter_factory({'super_admin_key': 'supertest'})(app) - self.assertEqual(ath.auth_prefix, '/auth/') - ath = auth.filter_factory({'super_admin_key': 'supertest', - 'auth_prefix': ''})(app) - self.assertEqual(ath.auth_prefix, '/auth/') - ath = auth.filter_factory({'super_admin_key': 'supertest', - 'auth_prefix': '/test/'})(app) - self.assertEqual(ath.auth_prefix, '/test/') - ath = auth.filter_factory({'super_admin_key': 'supertest', - 'auth_prefix': '/test'})(app) - self.assertEqual(ath.auth_prefix, '/test/') - ath = auth.filter_factory({'super_admin_key': 'supertest', - 'auth_prefix': 'test/'})(app) - self.assertEqual(ath.auth_prefix, '/test/') - ath = auth.filter_factory({'super_admin_key': 'supertest', - 'auth_prefix': 'test'})(app) - self.assertEqual(ath.auth_prefix, '/test/') - - def test_no_auth_type_init(self): - app = FakeApp() - ath = auth.filter_factory({})(app) - self.assertEqual(ath.auth_type, 'Plaintext') - - def test_valid_auth_type_init(self): - app = FakeApp() - ath = auth.filter_factory({'auth_type': 'sha1'})(app) - self.assertEqual(ath.auth_type, 'Sha1') - ath = auth.filter_factory({'auth_type': 'plaintext'})(app) - self.assertEqual(ath.auth_type, 'Plaintext') - - def test_invalid_auth_type_init(self): - app = FakeApp() - exc = None - try: - auth.filter_factory({'auth_type': 'NONEXISTANT'})(app) - except Exception as err: - exc = err - self.assertEqual(str(exc), - 'Invalid auth_type in config file: %s' % - 'Nonexistant') - - def test_default_swift_cluster_init(self): - app = FakeApp() - self.assertRaises(ValueError, auth.filter_factory({ - 'super_admin_key': 'supertest', - 'default_swift_cluster': 'local#badscheme://host/path'}), app) - ath = auth.filter_factory({'super_admin_key': 'supertest'})(app) - self.assertEqual(ath.default_swift_cluster, - 'local#http://127.0.0.1:8080/v1') - ath = auth.filter_factory({'super_admin_key': 'supertest', - 'default_swift_cluster': 'local#http://host/path'})(app) - self.assertEqual(ath.default_swift_cluster, - 'local#http://host/path') - ath = auth.filter_factory({'super_admin_key': 'supertest', - 'default_swift_cluster': 'local#https://host/path/'})(app) - self.assertEqual(ath.dsc_url, 'https://host/path') - self.assertEqual(ath.dsc_url2, 'https://host/path') - ath = auth.filter_factory({'super_admin_key': 'supertest', - 'default_swift_cluster': - 'local#https://host/path/#http://host2/path2/'})(app) - self.assertEqual(ath.dsc_url, 'https://host/path') - self.assertEqual(ath.dsc_url2, 'http://host2/path2') - - def test_credentials_match_auth_encoder_type(self): - plaintext_auth = {'auth': 'plaintext:key'} - sha1_key = ("sha1:T0YFdhqN4uDRWiYLxWa7H2T8AewG4fEYQyJFRLsgcfk=$46c58" - "07eb8a32e8f404fea9eaaeb60b7e1207ff1") - sha1_auth = {'auth': sha1_key} - sha512_key = ("sha512:aSm0jEeqIp46T5YLZy1r8+cXs/Xzs1S4VUwVauhBs44=$ef" - "7332ec1288bf69c75682eb8d459d5a84baa7e43f45949c242a9af9" - "7130ef16ac361fe1aa33a789e218122b83c54ef1923fc015080741" - "ca21f6187329f6cb7a") - sha512_auth = {'auth': sha512_key} - - # test all possible config settings work with all possible auth types - for auth_type in ('plaintext', 'sha1', 'sha512'): - test_auth = auth.filter_factory({'super_admin_key': 'superkey', - 'auth_type': auth_type})(FakeApp()) - for detail in (plaintext_auth, sha1_auth, sha512_auth): - self.assertTrue(test_auth.credentials_match(detail, 'key')) - # test invalid auth type stored - invalid_detail = {'auth': 'Junk:key'} - test_auth.logger = mock.Mock() - self.assertFalse(test_auth.credentials_match(invalid_detail, - 'key')) - # make sure error is logged - test_auth.logger.called_once_with('Invalid auth_type Junk') - - def test_top_level_denied(self): - resp = Request.blank('/').get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - - def test_anon(self): - resp = Request.blank('/v1/AUTH_account').get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - self.assertEqual(resp.environ['swift.authorize'], - self.test_auth.authorize) - - def test_auth_deny_non_reseller_prefix(self): - resp = Request.blank('/v1/BLAH_account', - headers={'X-Auth-Token': 'BLAH_t'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - self.assertEqual(resp.environ['swift.authorize'], - self.test_auth.denied_response) - - def test_auth_deny_non_reseller_prefix_no_override(self): - fake_authorize = lambda x: Response(status='500 Fake') - resp = Request.blank('/v1/BLAH_account', - headers={'X-Auth-Token': 'BLAH_t'}, - environ={'swift.authorize': fake_authorize} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(resp.environ['swift.authorize'], fake_authorize) - - def test_auth_no_reseller_prefix_deny(self): - # Ensures that when we have no reseller prefix, we don't deny a request - # outright but set up a denial swift.authorize and pass the request on - # down the chain. - local_app = FakeApp() - local_auth = auth.filter_factory({'super_admin_key': 'supertest', - 'reseller_prefix': ''})(local_app) - resp = Request.blank('/v1/account', - headers={'X-Auth-Token': 't'}).get_response(local_auth) - self.assertEqual(resp.status_int, 401) - # one for checking auth, two for request passed along - self.assertEqual(local_app.calls, 2) - self.assertEqual(resp.environ['swift.authorize'], - local_auth.denied_response) - - def test_auth_no_reseller_prefix_allow(self): - # Ensures that when we have no reseller prefix, we can still allow - # access if our auth server accepts requests - local_app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'account': 'act', 'user': 'act:usr', - 'account_id': 'AUTH_cfa', - 'groups': [{'name': 'act:usr'}, {'name': 'act'}, - {'name': '.admin'}], - 'expires': time() + 60})), - ('204 No Content', {}, '')])) - local_auth = auth.filter_factory({'super_admin_key': 'supertest', - 'reseller_prefix': ''})(local_app) - resp = Request.blank('/v1/act', - headers={'X-Auth-Token': 't'}).get_response(local_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(local_app.calls, 2) - self.assertEqual(resp.environ['swift.authorize'], - local_auth.authorize) - - def test_auth_no_reseller_prefix_no_token(self): - # Check that normally we set up a call back to our authorize. - local_auth = \ - auth.filter_factory({'super_admin_key': 'supertest', - 'reseller_prefix': ''})(FakeApp(iter([]))) - resp = Request.blank('/v1/account').get_response(local_auth) - self.assertEqual(resp.status_int, 401) - self.assertEqual(resp.environ['swift.authorize'], - local_auth.authorize) - # Now make sure we don't override an existing swift.authorize when we - # have no reseller prefix. - local_auth = \ - auth.filter_factory({'super_admin_key': 'supertest', - 'reseller_prefix': ''})(FakeApp()) - local_authorize = lambda req: Response('test') - resp = Request.blank('/v1/account', environ={'swift.authorize': - local_authorize}).get_response(local_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.environ['swift.authorize'], local_authorize) - - def test_auth_fail(self): - resp = Request.blank('/v1/AUTH_cfa', - headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - - def test_auth_success(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'account': 'act', 'user': 'act:usr', - 'account_id': 'AUTH_cfa', - 'groups': [{'name': 'act:usr'}, {'name': 'act'}, - {'name': '.admin'}], - 'expires': time() + 60})), - ('204 No Content', {}, '')])) - resp = Request.blank('/v1/AUTH_cfa', - headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_auth_memcache(self): - # First run our test without memcache, showing we need to return the - # token contents twice. - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'account': 'act', 'user': 'act:usr', - 'account_id': 'AUTH_cfa', - 'groups': [{'name': 'act:usr'}, {'name': 'act'}, - {'name': '.admin'}], - 'expires': time() + 60})), - ('204 No Content', {}, ''), - ('200 Ok', {}, - json.dumps({'account': 'act', 'user': 'act:usr', - 'account_id': 'AUTH_cfa', - 'groups': [{'name': 'act:usr'}, {'name': 'act'}, - {'name': '.admin'}], - 'expires': time() + 60})), - ('204 No Content', {}, '')])) - resp = Request.blank('/v1/AUTH_cfa', - headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - resp = Request.blank('/v1/AUTH_cfa', - headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 4) - # Now run our test with memcache, showing we no longer need to return - # the token contents twice. - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'account': 'act', 'user': 'act:usr', - 'account_id': 'AUTH_cfa', - 'groups': [{'name': 'act:usr'}, {'name': 'act'}, - {'name': '.admin'}], - 'expires': time() + 60})), - ('204 No Content', {}, ''), - # Don't need a second token object returned if memcache is used - ('204 No Content', {}, '')])) - fake_memcache = FakeMemcache() - resp = Request.blank('/v1/AUTH_cfa', - headers={'X-Auth-Token': 'AUTH_t'}, - environ={'swift.cache': fake_memcache} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - resp = Request.blank('/v1/AUTH_cfa', - headers={'X-Auth-Token': 'AUTH_t'}, - environ={'swift.cache': fake_memcache} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 3) - - def test_auth_just_expired(self): - self.test_auth.app = FakeApp(iter([ - # Request for token (which will have expired) - ('200 Ok', {}, - json.dumps({'account': 'act', 'user': 'act:usr', - 'account_id': 'AUTH_cfa', - 'groups': [{'name': 'act:usr'}, {'name': 'act'}, - {'name': '.admin'}], - 'expires': time() - 1})), - # Request to delete token - ('204 No Content', {}, '')])) - resp = Request.blank('/v1/AUTH_cfa', - headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_middleware_storage_token(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'account': 'act', 'user': 'act:usr', - 'account_id': 'AUTH_cfa', - 'groups': [{'name': 'act:usr'}, {'name': 'act'}, - {'name': '.admin'}], - 'expires': time() + 60})), - ('204 No Content', {}, '')])) - resp = Request.blank('/v1/AUTH_cfa', - headers={'X-Storage-Token': 'AUTH_t'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_authorize_bad_path(self): - req = Request.blank('/badpath') - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 401) - req = Request.blank('/badpath') - req.remote_user = 'act:usr,act,AUTH_cfa' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - - def test_authorize_account_access(self): - req = Request.blank('/v1/AUTH_cfa') - req.remote_user = 'act:usr,act,AUTH_cfa' - self.assertEqual(self.test_auth.authorize(req), None) - req = Request.blank('/v1/AUTH_cfa') - req.remote_user = 'act:usr,act' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - - def test_authorize_acl_group_access(self): - req = Request.blank('/v1/AUTH_cfa') - req.remote_user = 'act:usr,act' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - req = Request.blank('/v1/AUTH_cfa') - req.remote_user = 'act:usr,act' - req.acl = 'act' - self.assertEqual(self.test_auth.authorize(req), None) - req = Request.blank('/v1/AUTH_cfa') - req.remote_user = 'act:usr,act' - req.acl = 'act:usr' - self.assertEqual(self.test_auth.authorize(req), None) - req = Request.blank('/v1/AUTH_cfa') - req.remote_user = 'act:usr,act' - req.acl = 'act2' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - req = Request.blank('/v1/AUTH_cfa') - req.remote_user = 'act:usr,act' - req.acl = 'act:usr2' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - - def test_deny_cross_reseller(self): - # Tests that cross-reseller is denied, even if ACLs/group names match - req = Request.blank('/v1/OTHER_cfa') - req.remote_user = 'act:usr,act,AUTH_cfa' - req.acl = 'act' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - - def test_authorize_acl_referrer_access(self): - req = Request.blank('/v1/AUTH_cfa/c') - req.remote_user = 'act:usr,act' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - req = Request.blank('/v1/AUTH_cfa/c') - req.remote_user = 'act:usr,act' - req.acl = '.r:*,.rlistings' - self.assertEqual(self.test_auth.authorize(req), None) - req = Request.blank('/v1/AUTH_cfa/c') - req.remote_user = 'act:usr,act' - req.acl = '.r:*' # No listings allowed - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - req = Request.blank('/v1/AUTH_cfa/c') - req.remote_user = 'act:usr,act' - req.acl = '.r:.example.com,.rlistings' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - req = Request.blank('/v1/AUTH_cfa/c') - req.remote_user = 'act:usr,act' - req.referer = 'http://www.example.com/index.html' - req.acl = '.r:.example.com,.rlistings' - self.assertEqual(self.test_auth.authorize(req), None) - req = Request.blank('/v1/AUTH_cfa/c') - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 401) - req = Request.blank('/v1/AUTH_cfa/c') - req.acl = '.r:*,.rlistings' - self.assertEqual(self.test_auth.authorize(req), None) - req = Request.blank('/v1/AUTH_cfa/c') - req.acl = '.r:*' # No listings allowed - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 401) - req = Request.blank('/v1/AUTH_cfa/c') - req.acl = '.r:.example.com,.rlistings' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 401) - req = Request.blank('/v1/AUTH_cfa/c') - req.referer = 'http://www.example.com/index.html' - req.acl = '.r:.example.com,.rlistings' - self.assertEqual(self.test_auth.authorize(req), None) - - def test_detect_reseller_request(self): - req = self._make_request('/v1/AUTH_admin', - headers={'X-Auth-Token': 'AUTH_t'}) - cache_key = 'AUTH_/auth/AUTH_t' - cache_entry = (time() + 3600, '.reseller_admin') - req.environ['swift.cache'].set(cache_key, cache_entry) - req.get_response(self.test_auth) - self.assertTrue(req.environ.get('reseller_request')) - - def test_account_put_permissions(self): - req = Request.blank('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'}) - req.remote_user = 'act:usr,act' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - - req = Request.blank('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'}) - req.remote_user = 'act:usr,act,AUTH_other' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - - # Even PUTs to your own account as account admin should fail - req = Request.blank('/v1/AUTH_old', environ={'REQUEST_METHOD': 'PUT'}) - req.remote_user = 'act:usr,act,AUTH_old' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - - req = Request.blank('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'}) - req.remote_user = 'act:usr,act,.reseller_admin' - resp = self.test_auth.authorize(req) - self.assertEqual(resp, None) - - # .super_admin is not something the middleware should ever see or care - # about - req = Request.blank('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'}) - req.remote_user = 'act:usr,act,.super_admin' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - - def test_account_delete_permissions(self): - req = Request.blank('/v1/AUTH_new', - environ={'REQUEST_METHOD': 'DELETE'}) - req.remote_user = 'act:usr,act' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - - req = Request.blank('/v1/AUTH_new', - environ={'REQUEST_METHOD': 'DELETE'}) - req.remote_user = 'act:usr,act,AUTH_other' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - - # Even DELETEs to your own account as account admin should fail - req = Request.blank('/v1/AUTH_old', - environ={'REQUEST_METHOD': 'DELETE'}) - req.remote_user = 'act:usr,act,AUTH_old' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - - req = Request.blank('/v1/AUTH_new', - environ={'REQUEST_METHOD': 'DELETE'}) - req.remote_user = 'act:usr,act,.reseller_admin' - resp = self.test_auth.authorize(req) - self.assertEqual(resp, None) - - # .super_admin is not something the middleware should ever see or care - # about - req = Request.blank('/v1/AUTH_new', - environ={'REQUEST_METHOD': 'DELETE'}) - req.remote_user = 'act:usr,act,.super_admin' - resp = self.test_auth.authorize(req) - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - - def test_get_token_fail(self): - resp = Request.blank('/auth/v1.0').get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - resp = Request.blank('/auth/v1.0', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - - def test_get_token_fail_invalid_key(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]}))])) - resp = Request.blank('/auth/v1.0', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'invalid'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_get_token_fail_invalid_x_auth_user_format(self): - resp = Request.blank('/auth/v1/act/auth', - headers={'X-Auth-User': 'usr', - 'X-Auth-Key': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - - def test_get_token_fail_non_matching_account_in_request(self): - resp = Request.blank('/auth/v1/act/auth', - headers={'X-Auth-User': 'act2:usr', - 'X-Auth-Key': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - - def test_get_token_fail_bad_path(self): - resp = Request.blank('/auth/v1/act/auth/invalid', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - - def test_get_token_fail_missing_key(self): - resp = Request.blank('/auth/v1/act/auth', - headers={'X-Auth-User': 'act:usr'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - - def test_get_token_fail_get_user_details(self): - self.test_auth.app = FakeApp(iter([ - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v1.0', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_get_token_fail_get_account(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - # GET of account - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v1.0', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_get_token_fail_put_new_token(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - # GET of account - ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of new token - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v1.0', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 3) - - def test_get_token_fail_post_to_user(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - # GET of account - ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of new token - ('201 Created', {}, ''), - # POST of token to user object - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v1.0', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 4) - - def test_get_token_fail_get_services(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - # GET of account - ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of new token - ('201 Created', {}, ''), - # POST of token to user object - ('204 No Content', {}, ''), - # GET of services object - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v1.0', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 5) - - def test_get_token_fail_get_existing_token(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tktest'}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - # GET of token - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v1.0', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_get_token_success_v1_0(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - # GET of account - ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of new token - ('201 Created', {}, ''), - # POST of token to user object - ('204 No Content', {}, ''), - # GET of services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))])) - resp = Request.blank('/auth/v1.0', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertTrue(resp.headers.get('x-auth-token', - '').startswith('AUTH_tk'), resp.headers.get('x-auth-token')) - self.assertEqual(resp.headers.get('x-auth-token'), - resp.headers.get('x-storage-token')) - self.assertEqual(resp.headers.get('x-storage-url'), - 'http://127.0.0.1:8080/v1/AUTH_cfa') - self.assertEqual(json.loads(resp.body), - {"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) - self.assertEqual(self.test_auth.app.calls, 5) - - def test_get_token_success_v1_0_with_user_token_life(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - # GET of account - ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of new token - ('201 Created', {}, ''), - # POST of token to user object - ('204 No Content', {}, ''), - # GET of services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))])) - resp = Request.blank('/auth/v1.0', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'key', - 'X-Auth-Token-Lifetime': 10}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - left = int(resp.headers['x-auth-token-expires']) - self.assertTrue(left > 0, '%d > 0' % left) - self.assertTrue(left <= 10, '%d <= 10' % left) - self.assertTrue(resp.headers.get('x-auth-token', - '').startswith('AUTH_tk'), resp.headers.get('x-auth-token')) - self.assertEqual(resp.headers.get('x-auth-token'), - resp.headers.get('x-storage-token')) - self.assertEqual(resp.headers.get('x-storage-url'), - 'http://127.0.0.1:8080/v1/AUTH_cfa') - self.assertEqual(json.loads(resp.body), - {"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) - self.assertEqual(self.test_auth.app.calls, 5) - - def test_get_token_success_v1_0_with_user_token_life_past_max(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - # GET of account - ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of new token - ('201 Created', {}, ''), - # POST of token to user object - ('204 No Content', {}, ''), - # GET of services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))])) - req = Request.blank( - '/auth/v1.0', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'key', - 'X-Auth-Token-Lifetime': MAX_TOKEN_LIFE * 10}) - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - left = int(resp.headers['x-auth-token-expires']) - self.assertTrue(left > DEFAULT_TOKEN_LIFE, - '%d > %d' % (left, DEFAULT_TOKEN_LIFE)) - self.assertTrue(left <= MAX_TOKEN_LIFE, - '%d <= %d' % (left, MAX_TOKEN_LIFE)) - self.assertTrue(resp.headers.get('x-auth-token', - '').startswith('AUTH_tk'), resp.headers.get('x-auth-token')) - self.assertEqual(resp.headers.get('x-auth-token'), - resp.headers.get('x-storage-token')) - self.assertEqual(resp.headers.get('x-storage-url'), - 'http://127.0.0.1:8080/v1/AUTH_cfa') - self.assertEqual(json.loads(resp.body), - {"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) - self.assertEqual(self.test_auth.app.calls, 5) - - def test_get_token_success_v1_act_auth(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - # GET of account - ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of new token - ('201 Created', {}, ''), - # POST of token to user object - ('204 No Content', {}, ''), - # GET of services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))])) - resp = Request.blank('/auth/v1/act/auth', - headers={'X-Storage-User': 'usr', - 'X-Storage-Pass': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertTrue(resp.headers.get('x-auth-token', - '').startswith('AUTH_tk'), resp.headers.get('x-auth-token')) - self.assertEqual(resp.headers.get('x-auth-token'), - resp.headers.get('x-storage-token')) - self.assertEqual(resp.headers.get('x-storage-url'), - 'http://127.0.0.1:8080/v1/AUTH_cfa') - self.assertEqual(json.loads(resp.body), - {"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) - self.assertEqual(self.test_auth.app.calls, 5) - - def test_get_token_success_storage_instead_of_auth(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - # GET of account - ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of new token - ('201 Created', {}, ''), - # POST of token to user object - ('204 No Content', {}, ''), - # GET of services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))])) - resp = Request.blank('/auth/v1.0', - headers={'X-Storage-User': 'act:usr', - 'X-Storage-Pass': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertTrue(resp.headers.get('x-auth-token', - '').startswith('AUTH_tk'), resp.headers.get('x-auth-token')) - self.assertEqual(resp.headers.get('x-auth-token'), - resp.headers.get('x-storage-token')) - self.assertEqual(resp.headers.get('x-storage-url'), - 'http://127.0.0.1:8080/v1/AUTH_cfa') - self.assertEqual(json.loads(resp.body), - {"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) - self.assertEqual(self.test_auth.app.calls, 5) - - def test_get_token_success_v1_act_auth_auth_instead_of_storage(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - # GET of account - ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of new token - ('201 Created', {}, ''), - # POST of token to user object - ('204 No Content', {}, ''), - # GET of services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))])) - resp = Request.blank('/auth/v1/act/auth', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertTrue(resp.headers.get('x-auth-token', - '').startswith('AUTH_tk'), resp.headers.get('x-auth-token')) - self.assertEqual(resp.headers.get('x-auth-token'), - resp.headers.get('x-storage-token')) - self.assertEqual(resp.headers.get('x-storage-url'), - 'http://127.0.0.1:8080/v1/AUTH_cfa') - self.assertEqual(json.loads(resp.body), - {"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) - self.assertEqual(self.test_auth.app.calls, 5) - - def test_get_token_success_existing_token(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tktest'}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - # GET of token - ('200 Ok', {}, json.dumps({"account": "act", "user": "usr", - "account_id": "AUTH_cfa", "groups": [{'name': "act:usr"}, - {'name': "key"}, {'name': ".admin"}], - "expires": 9999999999.9999999})), - # GET of services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))])) - resp = Request.blank('/auth/v1.0', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertEqual(resp.headers.get('x-auth-token'), 'AUTH_tktest') - self.assertEqual(resp.headers.get('x-auth-token'), - resp.headers.get('x-storage-token')) - self.assertEqual(resp.headers.get('x-storage-url'), - 'http://127.0.0.1:8080/v1/AUTH_cfa') - self.assertEqual(json.loads(resp.body), - {"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) - self.assertEqual(self.test_auth.app.calls, 3) - - def test_get_token_success_existing_token_but_request_new_one(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tktest'}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - # DELETE of expired token - ('204 No Content', {}, ''), - # GET of account - ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of new token - ('201 Created', {}, ''), - # POST of token to user object - ('204 No Content', {}, ''), - # GET of services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))])) - resp = Request.blank('/auth/v1.0', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'key', - 'X-Auth-New-Token': 'true'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertNotEqual(resp.headers.get('x-auth-token'), 'AUTH_tktest') - self.assertEqual(resp.headers.get('x-auth-token'), - resp.headers.get('x-storage-token')) - self.assertEqual(resp.headers.get('x-storage-url'), - 'http://127.0.0.1:8080/v1/AUTH_cfa') - self.assertEqual(json.loads(resp.body), - {"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) - self.assertEqual(self.test_auth.app.calls, 6) - - def test_get_token_success_existing_token_expired(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tktest'}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - # GET of token - ('200 Ok', {}, json.dumps({"account": "act", "user": "usr", - "account_id": "AUTH_cfa", "groups": [{'name': "act:usr"}, - {'name': "key"}, {'name': ".admin"}], - "expires": 0.0})), - # DELETE of expired token - ('204 No Content', {}, ''), - # GET of account - ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of new token - ('201 Created', {}, ''), - # POST of token to user object - ('204 No Content', {}, ''), - # GET of services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))])) - resp = Request.blank('/auth/v1.0', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertNotEqual(resp.headers.get('x-auth-token'), 'AUTH_tktest') - self.assertEqual(resp.headers.get('x-auth-token'), - resp.headers.get('x-storage-token')) - self.assertEqual(resp.headers.get('x-storage-url'), - 'http://127.0.0.1:8080/v1/AUTH_cfa') - self.assertEqual(json.loads(resp.body), - {"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) - self.assertEqual(self.test_auth.app.calls, 7) - - def test_get_token_success_existing_token_expired_fail_deleting_old(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tktest'}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - # GET of token - ('200 Ok', {}, json.dumps({"account": "act", "user": "usr", - "account_id": "AUTH_cfa", "groups": [{'name': "act:usr"}, - {'name': "key"}, {'name': ".admin"}], - "expires": 0.0})), - # DELETE of expired token - ('503 Service Unavailable', {}, ''), - # GET of account - ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of new token - ('201 Created', {}, ''), - # POST of token to user object - ('204 No Content', {}, ''), - # GET of services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))])) - resp = Request.blank('/auth/v1.0', - headers={'X-Auth-User': 'act:usr', - 'X-Auth-Key': 'key'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertNotEqual(resp.headers.get('x-auth-token'), 'AUTH_tktest') - self.assertEqual(resp.headers.get('x-auth-token'), - resp.headers.get('x-storage-token')) - self.assertEqual(resp.headers.get('x-storage-url'), - 'http://127.0.0.1:8080/v1/AUTH_cfa') - self.assertEqual(json.loads(resp.body), - {"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) - self.assertEqual(self.test_auth.app.calls, 7) - - def test_prep_success(self): - list_to_iter = [ - # PUT of .auth account - ('201 Created', {}, ''), - # PUT of .account_id container - ('201 Created', {}, '')] - # PUT of .token* containers - for x in xrange(16): - list_to_iter.append(('201 Created', {}, '')) - self.test_auth.app = FakeApp(iter(list_to_iter)) - resp = Request.blank('/auth/v2/.prep', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 18) - - def test_prep_bad_method(self): - resp = Request.blank('/auth/v2/.prep', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - resp = Request.blank('/auth/v2/.prep', - environ={'REQUEST_METHOD': 'HEAD'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - resp = Request.blank('/auth/v2/.prep', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - - def test_prep_bad_creds(self): - resp = Request.blank('/auth/v2/.prep', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': 'super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - resp = Request.blank('/auth/v2/.prep', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'upertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - resp = Request.blank('/auth/v2/.prep', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': '.super_admin'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - resp = Request.blank('/auth/v2/.prep', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - resp = Request.blank('/auth/v2/.prep', - environ={'REQUEST_METHOD': 'POST'}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - - def test_prep_fail_account_create(self): - self.test_auth.app = FakeApp(iter([ - # PUT of .auth account - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/.prep', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_prep_fail_token_container_create(self): - self.test_auth.app = FakeApp(iter([ - # PUT of .auth account - ('201 Created', {}, ''), - # PUT of .token container - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/.prep', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_prep_fail_account_id_container_create(self): - self.test_auth.app = FakeApp(iter([ - # PUT of .auth account - ('201 Created', {}, ''), - # PUT of .token container - ('201 Created', {}, ''), - # PUT of .account_id container - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/.prep', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 3) - - def test_get_reseller_success(self): - self.test_auth.app = FakeApp(iter([ - # GET of .auth account (list containers) - ('200 Ok', {}, json.dumps([ - {"name": ".token", "count": 0, "bytes": 0}, - {"name": ".account_id", "count": 0, "bytes": 0}, - {"name": "act", "count": 0, "bytes": 0}])), - # GET of .auth account (list containers continuation) - ('200 Ok', {}, '[]')])) - resp = Request.blank('/auth/v2', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertEqual(json.loads(resp.body), - {"accounts": [{"name": "act"}]}) - self.assertEqual(self.test_auth.app.calls, 2) - - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"}, - {"name": "test"}, {"name": ".admin"}, - {"name": ".reseller_admin"}], "auth": "plaintext:key"})), - # GET of .auth account (list containers) - ('200 Ok', {}, json.dumps([ - {"name": ".token", "count": 0, "bytes": 0}, - {"name": ".account_id", "count": 0, "bytes": 0}, - {"name": "act", "count": 0, "bytes": 0}])), - # GET of .auth account (list containers continuation) - ('200 Ok', {}, '[]')])) - resp = Request.blank('/auth/v2', - headers={'X-Auth-Admin-User': 'act:adm', - 'X-Auth-Admin-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertEqual(json.loads(resp.body), - {"accounts": [{"name": "act"}]}) - self.assertEqual(self.test_auth.app.calls, 3) - - def test_get_reseller_fail_bad_creds(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2', - headers={'X-Auth-Admin-User': 'super:admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - self.assertEqual(self.test_auth.app.calls, 1) - - self.test_auth.app = FakeApp(iter([ - # GET of user object (account admin, but not reseller admin) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2', - headers={'X-Auth-Admin-User': 'act:adm', - 'X-Auth-Admin-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 1) - - self.test_auth.app = FakeApp(iter([ - # GET of user object (regular user) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2', - headers={'X-Auth-Admin-User': 'act:usr', - 'X-Auth-Admin-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_get_reseller_fail_listing(self): - self.test_auth.app = FakeApp(iter([ - # GET of .auth account (list containers) - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 1) - - self.test_auth.app = FakeApp(iter([ - # GET of .auth account (list containers) - ('200 Ok', {}, json.dumps([ - {"name": ".token", "count": 0, "bytes": 0}, - {"name": ".account_id", "count": 0, "bytes": 0}, - {"name": "act", "count": 0, "bytes": 0}])), - # GET of .auth account (list containers continuation) - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_get_account_success(self): - self.test_auth.app = FakeApp(iter([ - # GET of .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})), - # GET of account container (list objects) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}, - {"name": "tester", "hash": "etag", "bytes": 104, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.736680"}, - {"name": "tester3", "hash": "etag", "bytes": 86, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:28.135530"}])), - # GET of account container (list objects continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]')])) - resp = Request.blank('/auth/v2/act', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertEqual(json.loads(resp.body), - {'account_id': 'AUTH_cfa', - 'services': {'storage': - {'default': 'local', - 'local': 'http://127.0.0.1:8080/v1/AUTH_cfa'}}, - 'users': [{'name': 'tester'}, {'name': 'tester3'}]}) - self.assertEqual(self.test_auth.app.calls, 3) - - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"})), - # GET of .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})), - # GET of account container (list objects) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}, - {"name": "tester", "hash": "etag", "bytes": 104, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.736680"}, - {"name": "tester3", "hash": "etag", "bytes": 86, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:28.135530"}])), - # GET of account container (list objects continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]')])) - resp = Request.blank('/auth/v2/act', - headers={'X-Auth-Admin-User': 'act:adm', - 'X-Auth-Admin-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertEqual(json.loads(resp.body), - {'account_id': 'AUTH_cfa', - 'services': {'storage': - {'default': 'local', - 'local': 'http://127.0.0.1:8080/v1/AUTH_cfa'}}, - 'users': [{'name': 'tester'}, {'name': 'tester3'}]}) - self.assertEqual(self.test_auth.app.calls, 4) - - def test_get_account_fail_bad_account_name(self): - resp = Request.blank('/auth/v2/.token', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - resp = Request.blank('/auth/v2/.anything', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - - def test_get_account_fail_creds(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2/act', - headers={'X-Auth-Admin-User': 'super:admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - self.assertEqual(self.test_auth.app.calls, 1) - - self.test_auth.app = FakeApp(iter([ - # GET of user object (account admin, but wrong account) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act2:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act', - headers={'X-Auth-Admin-User': 'act2:adm', - 'X-Auth-Admin-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 1) - - self.test_auth.app = FakeApp(iter([ - # GET of user object (regular user) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act', - headers={'X-Auth-Admin-User': 'act:usr', - 'X-Auth-Admin-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_get_account_fail_get_services(self): - self.test_auth.app = FakeApp(iter([ - # GET of .services object - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 1) - - self.test_auth.app = FakeApp(iter([ - # GET of .services object - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2/act', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 404) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_get_account_fail_listing(self): - self.test_auth.app = FakeApp(iter([ - # GET of .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})), - # GET of account container (list objects) - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 2) - - self.test_auth.app = FakeApp(iter([ - # GET of .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})), - # GET of account container (list objects) - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2/act', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 404) - self.assertEqual(self.test_auth.app.calls, 2) - - self.test_auth.app = FakeApp(iter([ - # GET of .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})), - # GET of account container (list objects) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}, - {"name": "tester", "hash": "etag", "bytes": 104, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.736680"}, - {"name": "tester3", "hash": "etag", "bytes": 86, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:28.135530"}])), - # GET of account container (list objects continuation) - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 3) - - def test_set_services_new_service(self): - self.test_auth.app = FakeApp(iter([ - # GET of .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})), - # PUT of new .services object - ('204 No Content', {}, '')])) - resp = Request.blank('/auth/v2/act/.services', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'}, - body=json.dumps({'new_service': {'new_endpoint': 'new_value'}}) - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertEqual(json.loads(resp.body), - {'storage': {'default': 'local', - 'local': 'http://127.0.0.1:8080/v1/AUTH_cfa'}, - 'new_service': {'new_endpoint': 'new_value'}}) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_set_services_new_endpoint(self): - self.test_auth.app = FakeApp(iter([ - # GET of .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})), - # PUT of new .services object - ('204 No Content', {}, '')])) - resp = Request.blank('/auth/v2/act/.services', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'}, - body=json.dumps({'storage': {'new_endpoint': 'new_value'}}) - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertEqual(json.loads(resp.body), - {'storage': {'default': 'local', - 'local': 'http://127.0.0.1:8080/v1/AUTH_cfa', - 'new_endpoint': 'new_value'}}) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_set_services_update_endpoint(self): - self.test_auth.app = FakeApp(iter([ - # GET of .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})), - # PUT of new .services object - ('204 No Content', {}, '')])) - resp = Request.blank('/auth/v2/act/.services', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'}, - body=json.dumps({'storage': {'local': 'new_value'}}) - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertEqual(json.loads(resp.body), - {'storage': {'default': 'local', - 'local': 'new_value'}}) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_set_services_fail_bad_creds(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2/act/.services', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': 'super:admin', - 'X-Auth-Admin-Key': 'supertest'}, - body=json.dumps({'storage': {'local': 'new_value'}}) - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - self.assertEqual(self.test_auth.app.calls, 1) - - self.test_auth.app = FakeApp(iter([ - # GET of user object (account admin, but not reseller admin) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/.services', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': 'act:adm', - 'X-Auth-Admin-Key': 'key'}, - body=json.dumps({'storage': {'local': 'new_value'}}) - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 1) - - self.test_auth.app = FakeApp(iter([ - # GET of user object (regular user) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/.services', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': 'act:usr', - 'X-Auth-Admin-Key': 'key'}, - body=json.dumps({'storage': {'local': 'new_value'}}) - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_set_services_fail_bad_account_name(self): - resp = Request.blank('/auth/v2/.act/.services', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'}, - body=json.dumps({'storage': {'local': 'new_value'}}) - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - - def test_set_services_fail_bad_json(self): - resp = Request.blank('/auth/v2/act/.services', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'}, - body='garbage' - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - resp = Request.blank('/auth/v2/act/.services', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'}, - body='' - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - - def test_set_services_fail_get_services(self): - self.test_auth.app = FakeApp(iter([ - # GET of .services object - ('503 Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act/.services', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'}, - body=json.dumps({'new_service': {'new_endpoint': 'new_value'}}) - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 1) - - self.test_auth.app = FakeApp(iter([ - # GET of .services object - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2/act/.services', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'}, - body=json.dumps({'new_service': {'new_endpoint': 'new_value'}}) - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 404) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_set_services_fail_put_services(self): - self.test_auth.app = FakeApp(iter([ - # GET of .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})), - # PUT of new .services object - ('503 Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act/.services', - environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'}, - body=json.dumps({'new_service': {'new_endpoint': 'new_value'}}) - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_put_account_success(self): - conn = FakeConn(iter([ - # PUT of storage account itself - ('201 Created', {}, '')])) - self.test_auth.get_conn = lambda: conn - self.test_auth.app = FakeApp(iter([ - # Initial HEAD of account container to check for pre-existence - ('404 Not Found', {}, ''), - # PUT of account container - ('204 No Content', {}, ''), - # PUT of .account_id mapping object - ('204 No Content', {}, ''), - # PUT of .services object - ('204 No Content', {}, ''), - # POST to account container updating X-Container-Meta-Account-Id - ('204 No Content', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 201) - self.assertEqual(self.test_auth.app.calls, 5) - self.assertEqual(conn.calls, 1) - - def test_put_account_success_preexist_but_not_completed(self): - conn = FakeConn(iter([ - # PUT of storage account itself - ('201 Created', {}, '')])) - self.test_auth.get_conn = lambda: conn - self.test_auth.app = FakeApp(iter([ - # Initial HEAD of account container to check for pre-existence - # We're going to show it as existing this time, but with no - # X-Container-Meta-Account-Id, indicating a failed previous attempt - ('200 Ok', {}, ''), - # PUT of .account_id mapping object - ('204 No Content', {}, ''), - # PUT of .services object - ('204 No Content', {}, ''), - # POST to account container updating X-Container-Meta-Account-Id - ('204 No Content', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 201) - self.assertEqual(self.test_auth.app.calls, 4) - self.assertEqual(conn.calls, 1) - - def test_put_account_success_preexist_and_completed(self): - conn = FakeConn(iter([ - # PUT of storage account itself - ('201 Created', {}, '')])) - self.test_auth.get_conn = lambda: conn - self.test_auth.app = FakeApp(iter([ - # Initial HEAD of account container to check for pre-existence - # We're going to show it as existing this time, and with an - # X-Container-Meta-Account-Id, indicating it already exists - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 202) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_put_account_success_with_given_suffix(self): - conn = FakeConn(iter([ - # PUT of storage account itself - ('201 Created', {}, '')])) - self.test_auth.get_conn = lambda: conn - self.test_auth.app = FakeApp(iter([ - # Initial HEAD of account container to check for pre-existence - ('404 Not Found', {}, ''), - # PUT of account container - ('204 No Content', {}, ''), - # PUT of .account_id mapping object - ('204 No Content', {}, ''), - # PUT of .services object - ('204 No Content', {}, ''), - # POST to account container updating X-Container-Meta-Account-Id - ('204 No Content', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest', - 'X-Account-Suffix': 'test-suffix'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 201) - self.assertEqual(conn.request_path, '/v1/AUTH_test-suffix') - self.assertEqual(self.test_auth.app.calls, 5) - self.assertEqual(conn.calls, 1) - - def test_put_account_fail_bad_creds(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': 'super:admin', - 'X-Auth-Admin-Key': 'supertest'}, - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - self.assertEqual(self.test_auth.app.calls, 1) - - self.test_auth.app = FakeApp(iter([ - # GET of user object (account admin, but not reseller admin) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': 'act:adm', - 'X-Auth-Admin-Key': 'key'}, - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 1) - - self.test_auth.app = FakeApp(iter([ - # GET of user object (regular user) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': 'act:usr', - 'X-Auth-Admin-Key': 'key'}, - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_put_account_fail_invalid_account_name(self): - resp = Request.blank('/auth/v2/.act', - environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'}, - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - - def test_put_account_fail_on_storage_account_put(self): - conn = FakeConn(iter([ - # PUT of storage account itself - ('503 Service Unavailable', {}, '')])) - self.test_auth.get_conn = lambda: conn - self.test_auth.app = FakeApp(iter([ - ])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(conn.calls, 1) - self.assertEqual(self.test_auth.app.calls, 0) - - def test_put_account_fail_on_initial_account_head(self): - conn = FakeConn(iter([ - # PUT of storage account itself - ('201 Created', {}, '')])) - self.test_auth.get_conn = lambda: conn - self.test_auth.app = FakeApp(iter([ - # Initial HEAD of account container to check for pre-existence - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_put_account_fail_on_account_marker_put(self): - conn = FakeConn(iter([ - # PUT of storage account itself - ('201 Created', {}, '')])) - self.test_auth.get_conn = lambda: conn - self.test_auth.app = FakeApp(iter([ - # Initial HEAD of account container to check for pre-existence - ('404 Not Found', {}, ''), - # PUT of account container - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_put_account_fail_on_account_id_mapping(self): - conn = FakeConn(iter([ - # PUT of storage account itself - ('201 Created', {}, '')])) - self.test_auth.get_conn = lambda: conn - self.test_auth.app = FakeApp(iter([ - # Initial HEAD of account container to check for pre-existence - ('404 Not Found', {}, ''), - # PUT of account container - ('204 No Content', {}, ''), - # PUT of .account_id mapping object - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(conn.calls, 1) - self.assertEqual(self.test_auth.app.calls, 3) - - def test_put_account_fail_on_services_object(self): - conn = FakeConn(iter([ - # PUT of storage account itself - ('201 Created', {}, '')])) - self.test_auth.get_conn = lambda: conn - self.test_auth.app = FakeApp(iter([ - # Initial HEAD of account container to check for pre-existence - ('404 Not Found', {}, ''), - # PUT of account container - ('204 No Content', {}, ''), - # PUT of .account_id mapping object - ('204 No Content', {}, ''), - # PUT of .services object - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(conn.calls, 1) - self.assertEqual(self.test_auth.app.calls, 4) - - def test_put_account_fail_on_post_mapping(self): - conn = FakeConn(iter([ - # PUT of storage account itself - ('201 Created', {}, '')])) - self.test_auth.get_conn = lambda: conn - self.test_auth.app = FakeApp(iter([ - # Initial HEAD of account container to check for pre-existence - ('404 Not Found', {}, ''), - # PUT of account container - ('204 No Content', {}, ''), - # PUT of .account_id mapping object - ('204 No Content', {}, ''), - # PUT of .services object - ('204 No Content', {}, ''), - # POST to account container updating X-Container-Meta-Account-Id - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(conn.calls, 1) - self.assertEqual(self.test_auth.app.calls, 5) - - def test_delete_account_success(self): - conn = FakeConn(iter([ - # DELETE of storage account itself - ('204 No Content', {}, '')])) - self.test_auth.get_conn = lambda x: conn - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}])), - # Account's container listing, checking for users (continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'), - # GET the .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})), - # DELETE the .services object - ('204 No Content', {}, ''), - # DELETE the .account_id mapping object - ('204 No Content', {}, ''), - # DELETE the account container - ('204 No Content', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 6) - self.assertEqual(conn.calls, 1) - - def test_delete_account_success_missing_services(self): - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}])), - # Account's container listing, checking for users (continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'), - # GET the .services object - ('404 Not Found', {}, ''), - # DELETE the .account_id mapping object - ('204 No Content', {}, ''), - # DELETE the account container - ('204 No Content', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 5) - - def test_delete_account_success_missing_storage_account(self): - conn = FakeConn(iter([ - # DELETE of storage account itself - ('404 Not Found', {}, '')])) - self.test_auth.get_conn = lambda x: conn - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}])), - # Account's container listing, checking for users (continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'), - # GET the .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})), - # DELETE the .services object - ('204 No Content', {}, ''), - # DELETE the .account_id mapping object - ('204 No Content', {}, ''), - # DELETE the account container - ('204 No Content', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 6) - self.assertEqual(conn.calls, 1) - - def test_delete_account_success_missing_account_id_mapping(self): - conn = FakeConn(iter([ - # DELETE of storage account itself - ('204 No Content', {}, '')])) - self.test_auth.get_conn = lambda x: conn - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}])), - # Account's container listing, checking for users (continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'), - # GET the .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})), - # DELETE the .services object - ('204 No Content', {}, ''), - # DELETE the .account_id mapping object - ('404 Not Found', {}, ''), - # DELETE the account container - ('204 No Content', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 6) - self.assertEqual(conn.calls, 1) - - def test_delete_account_success_missing_account_container_at_end(self): - conn = FakeConn(iter([ - # DELETE of storage account itself - ('204 No Content', {}, '')])) - self.test_auth.get_conn = lambda x: conn - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}])), - # Account's container listing, checking for users (continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'), - # GET the .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})), - # DELETE the .services object - ('204 No Content', {}, ''), - # DELETE the .account_id mapping object - ('204 No Content', {}, ''), - # DELETE the account container - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 6) - self.assertEqual(conn.calls, 1) - - def test_delete_account_fail_bad_creds(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': 'super:admin', - 'X-Auth-Admin-Key': 'supertest'}, - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - self.assertEqual(self.test_auth.app.calls, 1) - - self.test_auth.app = FakeApp(iter([ - # GET of user object (account admin, but not reseller admin) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': 'act:adm', - 'X-Auth-Admin-Key': 'key'}, - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 1) - - self.test_auth.app = FakeApp(iter([ - # GET of user object (regular user) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': 'act:usr', - 'X-Auth-Admin-Key': 'key'}, - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_delete_account_fail_invalid_account_name(self): - resp = Request.blank('/auth/v2/.act', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - - def test_delete_account_fail_not_found(self): - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 404) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_delete_account_fail_not_found_concurrency(self): - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}])), - # Account's container listing, checking for users (continuation) - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 404) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_delete_account_fail_list_account(self): - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_delete_account_fail_list_account_concurrency(self): - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}])), - # Account's container listing, checking for users (continuation) - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_delete_account_fail_has_users(self): - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}, - {"name": "tester", "hash": "etag", "bytes": 104, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.736680"}]))])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 409) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_delete_account_fail_has_users2(self): - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}])), - # Account's container listing, checking for users (continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": "tester", "hash": "etag", "bytes": 104, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.736680"}]))])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 409) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_delete_account_fail_get_services(self): - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}])), - # Account's container listing, checking for users (continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'), - # GET the .services object - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 3) - - def test_delete_account_fail_delete_storage_account(self): - conn = FakeConn(iter([ - # DELETE of storage account itself - ('409 Conflict', {}, '')])) - self.test_auth.get_conn = lambda x: conn - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}])), - # Account's container listing, checking for users (continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'), - # GET the .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 409) - self.assertEqual(self.test_auth.app.calls, 3) - self.assertEqual(conn.calls, 1) - - def test_delete_account_fail_delete_storage_account2(self): - conn = FakeConn(iter([ - # DELETE of storage account itself - ('204 No Content', {}, ''), - # DELETE of storage account itself - ('409 Conflict', {}, '')])) - self.test_auth.get_conn = lambda x: conn - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}])), - # Account's container listing, checking for users (continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'), - # GET the .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa", - "other": "http://127.0.0.1:8080/v1/AUTH_cfa2"}}))])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 3) - self.assertEqual(conn.calls, 2) - - def test_delete_account_fail_delete_storage_account3(self): - conn = FakeConn(iter([ - # DELETE of storage account itself - ('503 Service Unavailable', {}, '')])) - self.test_auth.get_conn = lambda x: conn - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}])), - # Account's container listing, checking for users (continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'), - # GET the .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 3) - self.assertEqual(conn.calls, 1) - - def test_delete_account_fail_delete_storage_account4(self): - conn = FakeConn(iter([ - # DELETE of storage account itself - ('204 No Content', {}, ''), - # DELETE of storage account itself - ('503 Service Unavailable', {}, '')])) - self.test_auth.get_conn = lambda x: conn - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}])), - # Account's container listing, checking for users (continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'), - # GET the .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa", - "other": "http://127.0.0.1:8080/v1/AUTH_cfa2"}}))])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 3) - self.assertEqual(conn.calls, 2) - - def test_delete_account_fail_delete_services(self): - conn = FakeConn(iter([ - # DELETE of storage account itself - ('204 No Content', {}, '')])) - self.test_auth.get_conn = lambda x: conn - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}])), - # Account's container listing, checking for users (continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'), - # GET the .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})), - # DELETE the .services object - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 4) - self.assertEqual(conn.calls, 1) - - def test_delete_account_fail_delete_account_id_mapping(self): - conn = FakeConn(iter([ - # DELETE of storage account itself - ('204 No Content', {}, '')])) - self.test_auth.get_conn = lambda x: conn - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}])), - # Account's container listing, checking for users (continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'), - # GET the .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})), - # DELETE the .services object - ('204 No Content', {}, ''), - # DELETE the .account_id mapping object - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 5) - self.assertEqual(conn.calls, 1) - - def test_delete_account_fail_delete_account_container(self): - conn = FakeConn(iter([ - # DELETE of storage account itself - ('204 No Content', {}, '')])) - self.test_auth.get_conn = lambda x: conn - self.test_auth.app = FakeApp(iter([ - # Account's container listing, checking for users - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}])), - # Account's container listing, checking for users (continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'), - # GET the .services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})), - # DELETE the .services object - ('204 No Content', {}, ''), - # DELETE the .account_id mapping object - ('204 No Content', {}, ''), - # DELETE the account container - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act', - environ={'REQUEST_METHOD': 'DELETE', - 'swift.cache': FakeMemcache()}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 6) - self.assertEqual(conn.calls, 1) - - def test_get_user_success(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {}, json.dumps( - {"groups": [{"name": "act:usr"}, {"name": "act"}, - {"name": ".admin"}], - "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/usr', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertEqual(resp.body, json.dumps( - {"groups": [{"name": "act:usr"}, {"name": "act"}, - {"name": ".admin"}], - "auth": "plaintext:key"})) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_get_user_fail_no_super_admin_key(self): - local_auth = auth.filter_factory({})(FakeApp(iter([ - # GET of user object (but we should never get here) - ('200 Ok', {}, json.dumps( - {"groups": [{"name": "act:usr"}, {"name": "act"}, - {"name": ".admin"}], - "auth": "plaintext:key"}))]))) - resp = Request.blank('/auth/v2/act/usr', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(local_auth) - self.assertEqual(resp.status_int, 404) - self.assertEqual(local_auth.app.calls, 0) - - def test_get_user_groups_success(self): - self.test_auth.app = FakeApp(iter([ - # GET of account container (list objects) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}, - {"name": "tester", "hash": "etag", "bytes": 104, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.736680"}, - {"name": "tester3", "hash": "etag", "bytes": 86, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:28.135530"}])), - # GET of user object - ('200 Ok', {}, json.dumps( - {"groups": [{"name": "act:tester"}, {"name": "act"}, - {"name": ".admin"}], - "auth": "plaintext:key"})), - # GET of user object - ('200 Ok', {}, json.dumps( - {"groups": [{"name": "act:tester3"}, {"name": "act"}], - "auth": "plaintext:key3"})), - # GET of account container (list objects continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]')])) - resp = Request.blank('/auth/v2/act/.groups', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertEqual(resp.body, json.dumps( - {"groups": [{"name": ".admin"}, {"name": "act"}, - {"name": "act:tester"}, {"name": "act:tester3"}]})) - self.assertEqual(self.test_auth.app.calls, 4) - - def test_get_user_groups_success2(self): - self.test_auth.app = FakeApp(iter([ - # GET of account container (list objects) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}, - {"name": "tester", "hash": "etag", "bytes": 104, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.736680"}])), - # GET of user object - ('200 Ok', {}, json.dumps( - {"groups": [{"name": "act:tester"}, {"name": "act"}, - {"name": ".admin"}], - "auth": "plaintext:key"})), - # GET of account container (list objects continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": "tester3", "hash": "etag", "bytes": 86, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:28.135530"}])), - # GET of user object - ('200 Ok', {}, json.dumps( - {"groups": [{"name": "act:tester3"}, {"name": "act"}], - "auth": "plaintext:key3"})), - # GET of account container (list objects continuation) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]')])) - resp = Request.blank('/auth/v2/act/.groups', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertEqual(resp.body, json.dumps( - {"groups": [{"name": ".admin"}, {"name": "act"}, - {"name": "act:tester"}, {"name": "act:tester3"}]})) - self.assertEqual(self.test_auth.app.calls, 5) - - def test_get_user_fail_invalid_account(self): - resp = Request.blank('/auth/v2/.invalid/usr', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - - def test_get_user_fail_invalid_user(self): - resp = Request.blank('/auth/v2/act/.invalid', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - - def test_get_user_fail_bad_creds(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - headers={'X-Auth-Admin-User': 'super:admin', - 'X-Auth-Admin-Key': 'supertest'}, - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - self.assertEqual(self.test_auth.app.calls, 1) - - self.test_auth.app = FakeApp(iter([ - # GET of user object (regular user) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/usr', - headers={'X-Auth-Admin-User': 'act:usr', - 'X-Auth-Admin-Key': 'key'}, - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_get_user_account_admin_success(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object (account admin, but not reseller admin) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"})), - # GET of requested user object - ('200 Ok', {}, json.dumps( - {"groups": [{"name": "act:usr"}, {"name": "act"}], - "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/usr', - headers={'X-Auth-Admin-User': 'act:adm', - 'X-Auth-Admin-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertEqual(resp.body, json.dumps( - {"groups": [{"name": "act:usr"}, {"name": "act"}], - "auth": "plaintext:key"})) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_get_user_account_admin_fail_getting_account_admin(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object (account admin check) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"})), - # GET of requested user object [who is an .admin as well] - ('200 Ok', {}, json.dumps( - {"groups": [{"name": "act:usr"}, {"name": "act"}, - {"name": ".admin"}], - "auth": "plaintext:key"})), - # GET of user object (reseller admin check [and fail here]) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/usr', - headers={'X-Auth-Admin-User': 'act:adm', - 'X-Auth-Admin-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 3) - - def test_get_user_account_admin_fail_getting_reseller_admin(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object (account admin check) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"})), - # GET of requested user object [who is a .reseller_admin] - ('200 Ok', {}, json.dumps( - {"groups": [{"name": "act:usr"}, {"name": "act"}, - {"name": ".reseller_admin"}], - "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/usr', - headers={'X-Auth-Admin-User': 'act:adm', - 'X-Auth-Admin-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_get_user_reseller_admin_fail_getting_reseller_admin(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object (account admin check) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"}, - {"name": "test"}, {"name": ".reseller_admin"}], - "auth": "plaintext:key"})), - # GET of requested user object [who also is a .reseller_admin] - ('200 Ok', {}, json.dumps( - {"groups": [{"name": "act:usr"}, {"name": "act"}, - {"name": ".reseller_admin"}], - "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/usr', - headers={'X-Auth-Admin-User': 'act:adm', - 'X-Auth-Admin-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_get_user_super_admin_succeed_getting_reseller_admin(self): - self.test_auth.app = FakeApp(iter([ - # GET of requested user object - ('200 Ok', {}, json.dumps( - {"groups": [{"name": "act:usr"}, {"name": "act"}, - {"name": ".reseller_admin"}], - "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/usr', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertEqual(resp.body, json.dumps( - {"groups": [{"name": "act:usr"}, {"name": "act"}, - {"name": ".reseller_admin"}], - "auth": "plaintext:key"})) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_get_user_groups_not_found(self): - self.test_auth.app = FakeApp(iter([ - # GET of account container (list objects) - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2/act/.groups', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 404) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_get_user_groups_fail_listing(self): - self.test_auth.app = FakeApp(iter([ - # GET of account container (list objects) - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act/.groups', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_get_user_groups_fail_get_user(self): - self.test_auth.app = FakeApp(iter([ - # GET of account container (list objects) - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, - json.dumps([ - {"name": ".services", "hash": "etag", "bytes": 112, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.618110"}, - {"name": "tester", "hash": "etag", "bytes": 104, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:27.736680"}, - {"name": "tester3", "hash": "etag", "bytes": 86, - "content_type": "application/octet-stream", - "last_modified": "2010-12-03T17:16:28.135530"}])), - # GET of user object - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act/.groups', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_get_user_not_found(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 404) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_get_user_fail(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest', - 'X-Auth-User-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_put_user_fail_invalid_account(self): - resp = Request.blank('/auth/v2/.invalid/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest', - 'X-Auth-User-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - - def test_put_user_fail_invalid_user(self): - resp = Request.blank('/auth/v2/act/.usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest', - 'X-Auth-User-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - - def test_put_user_fail_no_user_key(self): - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - - def test_put_user_reseller_admin_fail_bad_creds(self): - self.test_auth.app = FakeApp(iter([ - # Checking if user is changing his own key. This is called. - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:rdm"}, - {"name": "test"}, {"name": ".admin"}, - {"name": ".reseller_admin"}], "auth": "plaintext:key"})), - # GET of user object (reseller admin) - # This shouldn't actually get called, checked - # below - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:rdm"}, - {"name": "test"}, {"name": ".admin"}, - {"name": ".reseller_admin"}], "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': 'act:rdm', - 'X-Auth-Admin-Key': 'key', - 'X-Auth-User-Key': 'key', - 'X-Auth-User-Reseller-Admin': 'true'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - self.assertEqual(self.test_auth.app.calls, 1) - - self.test_auth.app = FakeApp(iter([ - # Checking if user is changing his own key. This is called. - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"})), - # GET of user object (account admin, but not reseller admin) - # This shouldn't actually get called, checked - # below - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': 'act:adm', - 'X-Auth-Admin-Key': 'key', - 'X-Auth-User-Key': 'key', - 'X-Auth-User-Reseller-Admin': 'true'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - self.assertEqual(self.test_auth.app.calls, 1) - - self.test_auth.app = FakeApp(iter([ - # Checking if user is changing his own key. This is called. - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"})), - # GET of user object (regular user) - # This shouldn't actually get called, checked - # below - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': 'act:adm', - 'X-Auth-Admin-Key': 'key', - 'X-Auth-User-Key': 'key', - 'X-Auth-User-Reseller-Admin': 'true'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_put_user_account_admin_fail_bad_creds(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object (account admin, but wrong account) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act2:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"})), - # Checking if user is changing his own key. - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': 'act2:adm', - 'X-Auth-Admin-Key': 'key', - 'X-Auth-User-Key': 'key', - 'X-Auth-User-Admin': 'true'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 2) - - self.test_auth.app = FakeApp(iter([ - # GET of user object (regular user) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"})), - # Checking if user is changing his own key. - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': 'act:usr', - 'X-Auth-Admin-Key': 'key', - 'X-Auth-User-Key': 'key', - 'X-Auth-User-Admin': 'true'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_put_user_regular_fail_bad_creds(self): - self.test_auth.app = FakeApp(iter([ - # GET of user object (account admin, but wrong - # account) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act2:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"})), - # Checking if user is changing his own key. - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': 'act2:adm', - 'X-Auth-Admin-Key': 'key', - 'X-Auth-User-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 2) - - self.test_auth.app = FakeApp(iter([ - # GET of user object (regular user) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"})), - # Checking if user is changing his own key. - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act2/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': 'act:usr', - 'X-Auth-Admin-Key': 'key', - 'X-Auth-User-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_put_user_regular_success(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of user object - ('201 Created', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest', - 'X-Auth-User-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 201) - self.assertEqual(self.test_auth.app.calls, 2) - self.assertEqual(json.loads(self.test_auth.app.request.body), - {"groups": [{"name": "act:usr"}, {"name": "act"}], - "auth": "plaintext:key"}) - - def test_put_user_special_chars_success(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of user object - ('201 Created', {}, '')])) - resp = Request.blank('/auth/v2/act/u_s-r', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest', - 'X-Auth-User-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 201) - self.assertEqual(self.test_auth.app.calls, 2) - self.assertEqual(json.loads(self.test_auth.app.request.body), - {"groups": [{"name": "act:u_s-r"}, {"name": "act"}], - "auth": "plaintext:key"}) - - def test_put_user_account_admin_success(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of user object - ('201 Created', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest', - 'X-Auth-User-Key': 'key', - 'X-Auth-User-Admin': 'true'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 201) - self.assertEqual(self.test_auth.app.calls, 2) - self.assertEqual(json.loads(self.test_auth.app.request.body), - {"groups": [{"name": "act:usr"}, {"name": "act"}, - {"name": ".admin"}], - "auth": "plaintext:key"}) - - def test_put_user_reseller_admin_success(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of user object - ('201 Created', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest', - 'X-Auth-User-Key': 'key', - 'X-Auth-User-Reseller-Admin': 'true'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 201) - self.assertEqual(self.test_auth.app.calls, 2) - self.assertEqual(json.loads(self.test_auth.app.request.body), - {"groups": [{"name": "act:usr"}, {"name": "act"}, - {"name": ".admin"}, {"name": ".reseller_admin"}], - "auth": "plaintext:key"}) - - def test_put_user_fail_not_found(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of user object - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest', - 'X-Auth-User-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 404) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_put_user_fail(self): - self.test_auth.app = FakeApp(iter([ - # PUT of user object - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest', - 'X-Auth-User-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_put_user_key_hash(self): - key_hash = ("sha512:aSm0jEeqIp46T5YLZy1r8+cXs/Xzs1S4VUwVauhBs44=$ef" - "7332ec1288bf69c75682eb8d459d5a84baa7e43f45949c242a9af9" - "7130ef16ac361fe1aa33a789e218122b83c54ef1923fc015080741" - "ca21f6187329f6cb7a") - - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of user object - ('201 Created', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest', - 'X-Auth-User-Key-Hash': quote(key_hash)} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 201) - self.assertEqual(self.test_auth.app.calls, 2) - self.assertEqual(json.loads(self.test_auth.app.request.body), - {"groups": [{"name": "act:usr"}, {"name": "act"}], - "auth": key_hash}) - - def test_put_user_key_hash_wrong_type(self): - key_hash = "wrong_auth_type:1234" - - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of user object - ('201 Created', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest', - 'X-Auth-User-Key-Hash': quote(key_hash)} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - self.assertEqual(self.test_auth.app.calls, 0) - - def test_put_user_key_hash_wrong_format(self): - key_hash = "1234" - - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of user object - ('201 Created', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest', - 'X-Auth-User-Key-Hash': quote(key_hash)} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - self.assertEqual(self.test_auth.app.calls, 0) - - def test_delete_user_bad_creds(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, json.dumps({"groups": [{"name": "act2:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"})), - # GET of user object (account admin, but wrong account) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act2:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'X-Auth-Admin-User': 'act2:adm', - 'X-Auth-Admin-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 2) - - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"})), - # GET of user object (regular user) - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"}))])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'X-Auth-Admin-User': 'act:usr', - 'X-Auth-Admin-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_delete_reseller_admin_user_fail(self): - self.test_auth.app = FakeApp(iter([ - # is user being deleted a reseller_admin - ('200 Ok', {}, json.dumps({"groups": [{"name": "act2:re_adm"}, - {"name": "act2"}, {"name": ".admin"}, - {"name": ".reseller_admin"}], "auth": "plaintext:key"})), - # GET of user object - ('200 Ok', {}, json.dumps({"groups": [{"name": "act2:adm"}, - {"name": "act2"}, {"name": ".admin"}], - "auth": "plaintext:key"}))])) - - resp = Request.blank('/auth/v2/act2/re_adm', - environ={ - 'REQUEST_METHOD': 'DELETE'}, - headers={ - 'X-Auth-Admin-User': - 'act2:adm', - 'X-Auth-Admin-Key': 'key'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_delete_reseller_admin_user_success(self): - self.test_auth.app = FakeApp(iter([ - # is user being deleted a reseller_admin - ('200 Ok', {}, json.dumps({"groups": [{"name": "act2:re_adm"}, - {"name": "act2"}, {"name": ".admin"}, - {"name": ".reseller_admin"}], "auth": "plaintext:key"})), - # HEAD of user object - ('200 Ok', - {'X-Object-Meta-Auth-Token': 'AUTH_tk'}, ''), - # DELETE of token - ('204 No Content', {}, ''), - # DELETE of user object - ('204 No Content', {}, '')])) - - resp = Request.blank('/auth/v2/act2/re_adm', - environ={ - 'REQUEST_METHOD': 'DELETE'}, - headers={ - 'X-Auth-Admin-User': - '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 4) - - def test_delete_user_invalid_account(self): - resp = Request.blank('/auth/v2/.invalid/usr', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - - def test_delete_user_invalid_user(self): - resp = Request.blank('/auth/v2/act/.invalid', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - - def test_delete_user_not_found(self): - self.test_auth.app = FakeApp(iter([ - # HEAD of user object - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 404) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_delete_user_fail_head_user(self): - self.test_auth.app = FakeApp(iter([ - # HEAD of user object - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_delete_user_fail_delete_token(self): - self.test_auth.app = FakeApp(iter([ - # is user reseller_admin - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"})), - # HEAD of user object - ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tk'}, ''), - # DELETE of token - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 3) - - def test_delete_user_fail_delete_user(self): - self.test_auth.app = FakeApp(iter([ - # is user reseller_admin - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"})), - # HEAD of user object - ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tk'}, ''), - # DELETE of token - ('204 No Content', {}, ''), - # DELETE of user object - ('503 Service Unavailable', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - self.assertEqual(self.test_auth.app.calls, 4) - - def test_delete_user_success(self): - self.test_auth.app = FakeApp(iter([ - # is user reseller_admin - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"})), - # HEAD of user object - ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tk'}, ''), - # DELETE of token - ('204 No Content', {}, ''), - # DELETE of user object - ('204 No Content', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 4) - - def test_delete_user_success_missing_user_at_end(self): - self.test_auth.app = FakeApp(iter([ - # is user reseller_admin - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"})), - # HEAD of user object - ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tk'}, ''), - # DELETE of token - ('204 No Content', {}, ''), - # DELETE of user object - ('404 Not Found', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 4) - - def test_delete_user_success_missing_token(self): - self.test_auth.app = FakeApp(iter([ - # is user reseller_admin - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"})), - # HEAD of user object - ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tk'}, ''), - # DELETE of token - ('404 Not Found', {}, ''), - # DELETE of user object - ('204 No Content', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 4) - - def test_delete_user_success_no_token(self): - self.test_auth.app = FakeApp(iter([ - # is user reseller_admin - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"})), - # HEAD of user object - ('200 Ok', {}, ''), - # DELETE of user object - ('204 No Content', {}, '')])) - resp = Request.blank('/auth/v2/act/usr', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'} - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 3) - - def test_validate_token_bad_prefix(self): - resp = Request.blank('/auth/v2/.token/BAD_token') \ - .get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - - def test_validate_token_tmi(self): - resp = Request.blank('/auth/v2/.token/AUTH_token/tmi') \ - .get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - - def test_validate_token_bad_memcache(self): - fake_memcache = FakeMemcache() - fake_memcache.set('AUTH_/auth/AUTH_token', 'bogus') - resp = Request.blank('/auth/v2/.token/AUTH_token', - environ={'swift.cache': - fake_memcache}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 500) - - def test_validate_token_from_memcache(self): - fake_memcache = FakeMemcache() - fake_memcache.set('AUTH_/auth/AUTH_token', (time() + 1, 'act:usr,act')) - resp = Request.blank('/auth/v2/.token/AUTH_token', - environ={'swift.cache': - fake_memcache}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(resp.headers.get('x-auth-groups'), 'act:usr,act') - self.assertTrue(float(resp.headers['x-auth-ttl']) < 1, - resp.headers['x-auth-ttl']) - - def test_validate_token_from_memcache_expired(self): - fake_memcache = FakeMemcache() - fake_memcache.set('AUTH_/auth/AUTH_token', (time() - 1, 'act:usr,act')) - resp = Request.blank('/auth/v2/.token/AUTH_token', - environ={'swift.cache': - fake_memcache}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 404) - self.assertTrue('x-auth-groups' not in resp.headers) - self.assertTrue('x-auth-ttl' not in resp.headers) - - def test_validate_token_from_object(self): - self.test_auth.app = FakeApp(iter([ - # GET of token object - ('200 Ok', {}, json.dumps({'groups': [{'name': 'act:usr'}, - {'name': 'act'}], 'expires': time() + 1}))])) - resp = Request.blank('/auth/v2/.token/AUTH_token' - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 1) - self.assertEqual(resp.headers.get('x-auth-groups'), 'act:usr,act') - self.assertTrue(float(resp.headers['x-auth-ttl']) < 1, - resp.headers['x-auth-ttl']) - - def test_validate_token_from_object_expired(self): - self.test_auth.app = FakeApp(iter([ - # GET of token object - ('200 Ok', {}, json.dumps({'groups': 'act:usr,act', - 'expires': time() - 1})), - # DELETE of expired token object - ('204 No Content', {}, '')])) - resp = Request.blank('/auth/v2/.token/AUTH_token' - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 404) - self.assertEqual(self.test_auth.app.calls, 2) - - def test_validate_token_from_object_with_admin(self): - self.test_auth.app = FakeApp(iter([ - # GET of token object - ('200 Ok', {}, json.dumps({'account_id': 'AUTH_cfa', 'groups': - [{'name': 'act:usr'}, {'name': 'act'}, {'name': '.admin'}], - 'expires': time() + 1}))])) - resp = Request.blank('/auth/v2/.token/AUTH_token' - ).get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(self.test_auth.app.calls, 1) - self.assertEqual(resp.headers.get('x-auth-groups'), - 'act:usr,act,AUTH_cfa') - self.assertTrue(float(resp.headers['x-auth-ttl']) < 1, - resp.headers['x-auth-ttl']) - - def test_get_conn_default(self): - conn = self.test_auth.get_conn() - self.assertEqual(conn.__class__, auth.HTTPConnection) - self.assertEqual(conn.host, '127.0.0.1') - self.assertEqual(conn.port, 8080) - - def test_get_conn_default_https(self): - local_auth = auth.filter_factory({'super_admin_key': 'supertest', - 'default_swift_cluster': 'local#https://1.2.3.4/v1'})(FakeApp()) - conn = local_auth.get_conn() - self.assertEqual(conn.__class__, auth.HTTPSConnection) - self.assertEqual(conn.host, '1.2.3.4') - self.assertEqual(conn.port, 443) - - def test_get_conn_overridden(self): - local_auth = auth.filter_factory({'super_admin_key': 'supertest', - 'default_swift_cluster': 'local#https://1.2.3.4/v1'})(FakeApp()) - conn = \ - local_auth.get_conn(urlparsed=auth.urlparse('http://5.6.7.8/v1')) - self.assertEqual(conn.__class__, auth.HTTPConnection) - self.assertEqual(conn.host, '5.6.7.8') - self.assertEqual(conn.port, 80) - - def test_get_conn_overridden_https(self): - local_auth = auth.filter_factory({'super_admin_key': 'supertest', - 'default_swift_cluster': 'local#http://1.2.3.4/v1'})(FakeApp()) - conn = \ - local_auth.get_conn(urlparsed=auth.urlparse('https://5.6.7.8/v1')) - self.assertEqual(conn.__class__, auth.HTTPSConnection) - self.assertEqual(conn.host, '5.6.7.8') - self.assertEqual(conn.port, 443) - - def test_get_itoken_fail_no_memcache(self): - exc = None - try: - self.test_auth.get_itoken({}) - except Exception as err: - exc = err - self.assertEqual(str(exc), - 'No memcache set up; required for Swauth middleware') - - def test_get_itoken_success(self): - fmc = FakeMemcache() - itk = self.test_auth.get_itoken({'swift.cache': fmc}) - self.assertTrue(itk.startswith('AUTH_itk'), itk) - expires, groups = fmc.get('AUTH_/auth/%s' % itk) - self.assertTrue(expires > time(), expires) - self.assertEqual(groups, '.auth,.reseller_admin,AUTH_.auth') - - def test_get_admin_detail_fail_no_colon(self): - self.test_auth.app = FakeApp(iter([])) - self.assertEqual(self.test_auth.get_admin_detail(Request.blank('/')), - None) - self.assertEqual(self.test_auth.get_admin_detail(Request.blank('/', - headers={'X-Auth-Admin-User': 'usr'})), None) - self.assertRaises(StopIteration, self.test_auth.get_admin_detail, - Request.blank('/', headers={'X-Auth-Admin-User': 'act:usr'})) - - def test_get_admin_detail_fail_user_not_found(self): - self.test_auth.app = FakeApp(iter([('404 Not Found', {}, '')])) - self.assertEqual(self.test_auth.get_admin_detail(Request.blank('/', - headers={'X-Auth-Admin-User': 'act:usr'})), None) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_get_admin_detail_fail_get_user_error(self): - self.test_auth.app = FakeApp(iter([ - ('503 Service Unavailable', {}, '')])) - exc = None - try: - self.test_auth.get_admin_detail(Request.blank('/', - headers={'X-Auth-Admin-User': 'act:usr'})) - except Exception as err: - exc = err - self.assertEqual(str(exc), 'Could not get user object: ' - '/v1/AUTH_.auth/act/usr 503 Service Unavailable') - self.assertEqual(self.test_auth.app.calls, 1) - - def test_get_admin_detail_success(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]}))])) - detail = self.test_auth.get_admin_detail(Request.blank('/', - headers={'X-Auth-Admin-User': 'act:usr'})) - self.assertEqual(self.test_auth.app.calls, 1) - self.assertEqual(detail, {'account': 'act', - 'auth': 'plaintext:key', - 'groups': [{'name': 'act:usr'}, {'name': 'act'}, - {'name': '.admin'}]}) - - def test_get_user_detail_success(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]}))])) - detail = self.test_auth.get_user_detail( - Request.blank('/', - headers={'X-Auth-Admin-User': 'act:usr'}), - 'act', 'usr') - self.assertEqual(self.test_auth.app.calls, 1) - detail_json = json.loads(detail) - self.assertEqual("plaintext:key", detail_json['auth']) - - def test_get_user_detail_fail_user_doesnt_exist(self): - self.test_auth.app = FakeApp( - iter([('404 Not Found', {}, '')])) - detail = self.test_auth.get_user_detail( - Request.blank('/', - headers={'X-Auth-Admin-User': 'act:usr'}), - 'act', 'usr') - self.assertEqual(self.test_auth.app.calls, 1) - self.assertEqual(detail, None) - - def test_get_user_detail_fail_exception(self): - self.test_auth.app = FakeApp(iter([ - ('503 Service Unavailable', {}, '')])) - exc = None - try: - self.test_auth.get_user_detail( - Request.blank('/', - headers={'X-Auth-Admin-User': 'act:usr'}), - 'act', 'usr') - except Exception as err: - exc = err - self.assertEqual(str(exc), 'Could not get user object: ' - '/v1/AUTH_.auth/act/usr 503 Service Unavailable') - self.assertEqual(self.test_auth.app.calls, 1) - - def test_is_user_reseller_admin_success(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".reseller_admin"}]}))])) - result = self.test_auth.is_user_reseller_admin( - Request.blank('/', - headers={'X-Auth-Admin-User': 'act:usr'}), - 'act', 'usr') - self.assertEqual(self.test_auth.app.calls, 1) - self.assertTrue(result) - - def test_is_user_reseller_admin_fail(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({"auth": "plaintext:key", - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]}))])) - result = self.test_auth.is_user_reseller_admin( - Request.blank('/', - headers={'X-Auth-Admin-User': 'act:usr'}), - 'act', 'usr') - self.assertEqual(self.test_auth.app.calls, 1) - self.assertFalse(result) - - def test_is_user_reseller_admin_fail_user_doesnt_exist(self): - self.test_auth.app = FakeApp( - iter([('404 Not Found', {}, '')])) - req = Request.blank('/', headers={'X-Auth-Admin-User': 'act:usr'}) - result = self.test_auth.is_user_reseller_admin(req, 'act', 'usr') - self.assertEqual(self.test_auth.app.calls, 1) - self.assertFalse(result) - self.assertFalse(req.credentials_valid) - - def test_credentials_match_success(self): - self.assertTrue(self.test_auth.credentials_match( - {'auth': 'plaintext:key'}, 'key')) - - def test_credentials_match_fail_no_details(self): - self.assertTrue(not self.test_auth.credentials_match(None, 'notkey')) - - def test_credentials_match_fail_plaintext(self): - self.assertTrue(not self.test_auth.credentials_match( - {'auth': 'plaintext:key'}, 'notkey')) - - def test_is_user_changing_own_key_err(self): - # User does not exist - self.test_auth.app = FakeApp( - iter([('404 Not Found', {}, '')])) - req = Request.blank('/auth/v2/act/usr', - environ={ - 'REQUEST_METHOD': 'PUT'}, - headers={ - 'X-Auth-Admin-User': 'act:usr', - 'X-Auth-Admin-Key': 'key', - 'X-Auth-User-Key': 'key'}) - self.assertTrue( - not self.test_auth.is_user_changing_own_key(req, 'act:usr')) - self.assertEqual(self.test_auth.app.calls, 1) - - # user attempting to escalate himself as admin - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"}))])) - req = Request.blank('/auth/v2/act/usr', - environ={ - 'REQUEST_METHOD': 'PUT'}, - headers={ - 'X-Auth-Admin-User': 'act:usr', - 'X-Auth-Admin-Key': 'key', - 'X-Auth-User-Key': 'key', - 'X-Auth-User-Admin': 'true'}) - self.assertTrue( - not self.test_auth.is_user_changing_own_key(req, 'act:usr')) - self.assertEqual(self.test_auth.app.calls, 1) - - # admin attempting to escalate himself as reseller_admin - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"}, - {"name": "test"}, {"name": ".admin"}], - "auth": "plaintext:key"}))])) - req = Request.blank('/auth/v2/act/adm', - environ={ - 'REQUEST_METHOD': 'PUT'}, - headers={ - 'X-Auth-Admin-User': 'act:adm', - 'X-Auth-Admin-Key': 'key', - 'X-Auth-User-Key': 'key', - 'X-Auth-User-Reseller-Admin': 'true'}) - self.assertTrue( - not self.test_auth.is_user_changing_own_key(req, 'act:adm')) - self.assertEqual(self.test_auth.app.calls, 1) - - # different user - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"}))])) - req = Request.blank('/auth/v2/act/usr2', - environ={ - 'REQUEST_METHOD': 'PUT'}, - headers={ - 'X-Auth-Admin-User': 'act:usr', - 'X-Auth-Admin-Key': 'key', - 'X-Auth-User-Key': 'key'}) - self.assertTrue( - not self.test_auth.is_user_changing_own_key(req, 'act:usr2')) - self.assertEqual(self.test_auth.app.calls, 1) - - # wrong key - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"}, - {"name": "test"}], "auth": "plaintext:key"}))])) - req = Request.blank('/auth/v2/act/usr', - environ={ - 'REQUEST_METHOD': 'PUT'}, - headers={ - 'X-Auth-Admin-User': 'act:usr', - 'X-Auth-Admin-Key': 'wrongkey', - 'X-Auth-User-Key': 'newkey'}) - self.assertTrue( - not self.test_auth.is_user_changing_own_key(req, 'act:usr')) - self.assertEqual(self.test_auth.app.calls, 1) - - def test_is_super_admin_success(self): - self.assertTrue(self.test_auth.is_super_admin(Request.blank('/', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'}))) - - def test_is_super_admin_fail_bad_key(self): - self.assertTrue(not self.test_auth.is_super_admin(Request.blank('/', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'bad'}))) - self.assertTrue(not self.test_auth.is_super_admin(Request.blank('/', - headers={'X-Auth-Admin-User': '.super_admin'}))) - self.assertTrue(not self.test_auth.is_super_admin(Request.blank('/'))) - - def test_is_super_admin_fail_bad_user(self): - self.assertTrue(not self.test_auth.is_super_admin(Request.blank('/', - headers={'X-Auth-Admin-User': 'bad', - 'X-Auth-Admin-Key': 'supertest'}))) - self.assertTrue(not self.test_auth.is_super_admin(Request.blank('/', - headers={'X-Auth-Admin-Key': 'supertest'}))) - self.assertTrue(not self.test_auth.is_super_admin(Request.blank('/'))) - - def test_is_reseller_admin_success_is_super_admin(self): - self.assertTrue(self.test_auth.is_reseller_admin(Request.blank('/', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'}))) - - def test_is_reseller_admin_success_called_get_admin_detail(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'auth': 'plaintext:key', - 'groups': [{'name': 'act:rdm'}, {'name': 'act'}, - {'name': '.admin'}, - {'name': '.reseller_admin'}]}))])) - self.assertTrue(self.test_auth.is_reseller_admin(Request.blank('/', - headers={'X-Auth-Admin-User': 'act:rdm', - 'X-Auth-Admin-Key': 'key'}))) - - def test_is_reseller_admin_fail_only_account_admin(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'auth': 'plaintext:key', - 'groups': [{'name': 'act:adm'}, {'name': 'act'}, - {'name': '.admin'}]}))])) - self.assertTrue(not self.test_auth.is_reseller_admin(Request.blank('/', - headers={'X-Auth-Admin-User': 'act:adm', - 'X-Auth-Admin-Key': 'key'}))) - - def test_is_reseller_admin_fail_regular_user(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'auth': 'plaintext:key', - 'groups': [{'name': 'act:usr'}, {'name': 'act'}]}))])) - self.assertTrue(not self.test_auth.is_reseller_admin(Request.blank('/', - headers={'X-Auth-Admin-User': 'act:usr', - 'X-Auth-Admin-Key': 'key'}))) - - def test_is_reseller_admin_fail_bad_key(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'auth': 'plaintext:key', - 'groups': [{'name': 'act:rdm'}, {'name': 'act'}, - {'name': '.admin'}, - {'name': '.reseller_admin'}]}))])) - self.assertTrue(not self.test_auth.is_reseller_admin(Request.blank('/', - headers={'X-Auth-Admin-User': 'act:rdm', - 'X-Auth-Admin-Key': 'bad'}))) - - def test_is_account_admin_success_is_super_admin(self): - self.assertTrue(self.test_auth.is_account_admin(Request.blank('/', - headers={'X-Auth-Admin-User': '.super_admin', - 'X-Auth-Admin-Key': 'supertest'}), 'act')) - - def test_is_account_admin_success_is_reseller_admin(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'auth': 'plaintext:key', - 'groups': [{'name': 'act:rdm'}, {'name': 'act'}, - {'name': '.admin'}, - {'name': '.reseller_admin'}]}))])) - self.assertTrue(self.test_auth.is_account_admin(Request.blank('/', - headers={'X-Auth-Admin-User': 'act:rdm', - 'X-Auth-Admin-Key': 'key'}), 'act')) - - def test_is_account_admin_success(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'auth': 'plaintext:key', - 'groups': [{'name': 'act:adm'}, {'name': 'act'}, - {'name': '.admin'}]}))])) - self.assertTrue(self.test_auth.is_account_admin(Request.blank('/', - headers={'X-Auth-Admin-User': 'act:adm', - 'X-Auth-Admin-Key': 'key'}), 'act')) - - def test_is_account_admin_fail_account_admin_different_account(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'auth': 'plaintext:key', - 'groups': [{'name': 'act2:adm'}, {'name': 'act2'}, - {'name': '.admin'}]}))])) - self.assertTrue(not self.test_auth.is_account_admin(Request.blank('/', - headers={'X-Auth-Admin-User': 'act2:adm', - 'X-Auth-Admin-Key': 'key'}), 'act')) - - def test_is_account_admin_fail_regular_user(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'auth': 'plaintext:key', - 'groups': [{'name': 'act:usr'}, {'name': 'act'}]}))])) - self.assertTrue(not self.test_auth.is_account_admin(Request.blank('/', - headers={'X-Auth-Admin-User': 'act:usr', - 'X-Auth-Admin-Key': 'key'}), 'act')) - - def test_is_account_admin_fail_bad_key(self): - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'auth': 'plaintext:key', - 'groups': [{'name': 'act:rdm'}, {'name': 'act'}, - {'name': '.admin'}, - {'name': '.reseller_admin'}]}))])) - self.assertTrue(not self.test_auth.is_account_admin(Request.blank('/', - headers={'X-Auth-Admin-User': 'act:rdm', - 'X-Auth-Admin-Key': 'bad'}), 'act')) - - def test_reseller_admin_but_account_is_internal_use_only(self): - req = Request.blank('/v1/AUTH_.auth', - environ={'REQUEST_METHOD': 'GET'}) - req.remote_user = 'act:usr,act,.reseller_admin' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - - def test_reseller_admin_but_account_is_exactly_reseller_prefix(self): - req = Request.blank('/v1/AUTH_', environ={'REQUEST_METHOD': 'GET'}) - req.remote_user = 'act:usr,act,.reseller_admin' - resp = self.test_auth.authorize(req) - self.assertEqual(resp.status_int, 403) - - def _get_token_success_v1_0_encoded(self, saved_user, saved_key, sent_user, - sent_key): - self.test_auth.app = FakeApp(iter([ - # GET of user object - ('200 Ok', {}, - json.dumps({"auth": "plaintext:%s" % saved_key, - "groups": [{'name': saved_user}, {'name': "act"}, - {'name': ".admin"}]})), - # GET of account - ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), - # PUT of new token - ('201 Created', {}, ''), - # POST of token to user object - ('204 No Content', {}, ''), - # GET of services object - ('200 Ok', {}, json.dumps({"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))])) - resp = Request.blank('/auth/v1.0', - headers={'X-Auth-User': sent_user, - 'X-Auth-Key': sent_key}).get_response(self.test_auth) - self.assertEqual(resp.status_int, 200) - self.assertEqual(resp.content_type, CONTENT_TYPE_JSON) - self.assertTrue(resp.headers.get('x-auth-token', - '').startswith('AUTH_tk'), resp.headers.get('x-auth-token')) - self.assertEqual(resp.headers.get('x-auth-token'), - resp.headers.get('x-storage-token')) - self.assertEqual(resp.headers.get('x-storage-url'), - 'http://127.0.0.1:8080/v1/AUTH_cfa') - self.assertEqual(json.loads(resp.body), - {"storage": {"default": "local", - "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) - self.assertEqual(self.test_auth.app.calls, 5) - - def test_get_token_success_v1_0_encoded1(self): - self._get_token_success_v1_0_encoded( - 'act:usr', 'key', 'act%3ausr', 'key') - - def test_get_token_success_v1_0_encoded2(self): - self._get_token_success_v1_0_encoded( - 'act:u s r', 'key', 'act%3au%20s%20r', 'key') - - def test_get_token_success_v1_0_encoded3(self): - self._get_token_success_v1_0_encoded( - 'act:u s r', 'k:e:y', 'act%3au%20s%20r', 'k%3Ae%3ay') - - def test_allowed_sync_hosts(self): - a = auth.filter_factory({'super_admin_key': 'supertest'})(FakeApp()) - self.assertEqual(a.allowed_sync_hosts, ['127.0.0.1']) - a = auth.filter_factory({'super_admin_key': 'supertest', - 'allowed_sync_hosts': - '1.1.1.1,2.1.1.1, 3.1.1.1 , 4.1.1.1,, , 5.1.1.1'})(FakeApp()) - self.assertEqual(a.allowed_sync_hosts, - ['1.1.1.1', '2.1.1.1', '3.1.1.1', '4.1.1.1', '5.1.1.1']) - - def test_reseller_admin_is_owner(self): - orig_authorize = self.test_auth.authorize - owner_values = [] - - def mitm_authorize(req): - rv = orig_authorize(req) - owner_values.append(req.environ.get('swift_owner', False)) - return rv - - self.test_auth.authorize = mitm_authorize - - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'account': 'other', 'user': 'other:usr', - 'account_id': 'AUTH_other', - 'groups': [{'name': 'other:usr'}, {'name': 'other'}, - {'name': '.reseller_admin'}], - 'expires': time() + 60})), - ('204 No Content', {}, '')])) - req = Request.blank('/v1/AUTH_cfa', headers={'X-Auth-Token': 'AUTH_t'}) - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(owner_values, [True]) - - def test_admin_is_owner(self): - orig_authorize = self.test_auth.authorize - owner_values = [] - - def mitm_authorize(req): - rv = orig_authorize(req) - owner_values.append(req.environ.get('swift_owner', False)) - return rv - - self.test_auth.authorize = mitm_authorize - - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'account': 'act', 'user': 'act:usr', - 'account_id': 'AUTH_cfa', - 'groups': [{'name': 'act:usr'}, {'name': 'act'}, - {'name': '.admin'}], - 'expires': time() + 60})), - ('204 No Content', {}, '')])) - req = Request.blank('/v1/AUTH_cfa', headers={'X-Auth-Token': 'AUTH_t'}) - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(owner_values, [True]) - - def test_regular_is_not_owner(self): - orig_authorize = self.test_auth.authorize - owner_values = [] - - def mitm_authorize(req): - rv = orig_authorize(req) - owner_values.append(req.environ.get('swift_owner', False)) - return rv - - self.test_auth.authorize = mitm_authorize - - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({'account': 'act', 'user': 'act:usr', - 'account_id': 'AUTH_cfa', - 'groups': [{'name': 'act:usr'}, {'name': 'act'}], - 'expires': time() + 60})), - ('204 No Content', {}, '')]), acl='act:usr') - req = Request.blank('/v1/AUTH_cfa/c', - headers={'X-Auth-Token': 'AUTH_t'}) - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - self.assertEqual(owner_values, [False]) - - def test_sync_request_success(self): - self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), - sync_key='secret') - req = Request.blank('/v1/AUTH_cfa/c/o', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'x-container-sync-key': 'secret', - 'x-timestamp': '123.456'}) - req.remote_addr = '127.0.0.1' - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - - def test_sync_request_fail_key(self): - self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), - sync_key='secret') - req = Request.blank('/v1/AUTH_cfa/c/o', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'x-container-sync-key': 'wrongsecret', - 'x-timestamp': '123.456'}) - req.remote_addr = '127.0.0.1' - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - - self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), - sync_key='othersecret') - req = Request.blank('/v1/AUTH_cfa/c/o', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'x-container-sync-key': 'secret', - 'x-timestamp': '123.456'}) - req.remote_addr = '127.0.0.1' - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - - self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), - sync_key=None) - req = Request.blank('/v1/AUTH_cfa/c/o', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'x-container-sync-key': 'secret', - 'x-timestamp': '123.456'}) - req.remote_addr = '127.0.0.1' - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - - def test_sync_request_fail_no_timestamp(self): - self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), - sync_key='secret') - req = Request.blank('/v1/AUTH_cfa/c/o', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'x-container-sync-key': 'secret'}) - req.remote_addr = '127.0.0.1' - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - - def test_sync_request_fail_sync_host(self): - self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), - sync_key='secret') - req = Request.blank('/v1/AUTH_cfa/c/o', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'x-container-sync-key': 'secret', - 'x-timestamp': '123.456'}) - req.remote_addr = '127.0.0.2' - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - - def test_sync_request_success_lb_sync_host(self): - self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), - sync_key='secret') - req = Request.blank('/v1/AUTH_cfa/c/o', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'x-container-sync-key': 'secret', - 'x-timestamp': '123.456', - 'x-forwarded-for': '127.0.0.1'}) - req.remote_addr = '127.0.0.2' - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - - self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), - sync_key='secret') - req = Request.blank('/v1/AUTH_cfa/c/o', - environ={'REQUEST_METHOD': 'DELETE'}, - headers={'x-container-sync-key': 'secret', - 'x-timestamp': '123.456', - 'x-cluster-client-ip': '127.0.0.1'}) - req.remote_addr = '127.0.0.2' - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 204) - - def _make_request(self, path, **kwargs): - req = Request.blank(path, **kwargs) - req.environ['swift.cache'] = FakeMemcache() - return req - - def test_override_asked_for_but_not_allowed(self): - self.test_auth = \ - auth.filter_factory({'allow_overrides': 'false'})(FakeApp()) - req = self._make_request('/v1/AUTH_account', - environ={'swift.authorize_override': True}) - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - self.assertEqual(resp.environ['swift.authorize'], - self.test_auth.authorize) - - def test_override_asked_for_and_allowed(self): - self.test_auth = \ - auth.filter_factory({'allow_overrides': 'true'})(FakeApp()) - req = self._make_request('/v1/AUTH_account', - environ={'swift.authorize_override': True}) - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 404) - self.assertTrue('swift.authorize' not in resp.environ) - - def test_override_default_allowed(self): - req = self._make_request('/v1/AUTH_account', - environ={'swift.authorize_override': True}) - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 404) - self.assertTrue('swift.authorize' not in resp.environ) - - def test_token_too_long(self): - req = self._make_request('/v1/AUTH_account', headers={ - 'x-auth-token': 'a' * MAX_TOKEN_LENGTH}) - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - self.assertNotEqual(resp.body, 'Token exceeds maximum length.') - req = self._make_request('/v1/AUTH_account', headers={ - 'x-auth-token': 'a' * (MAX_TOKEN_LENGTH + 1)}) - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) - self.assertEqual(resp.body, 'Token exceeds maximum length.') - - def test_s3_enabled_when_conditions_are_met(self): - # auth_type_salt needs to be set - for atype in ('Sha1', 'Sha512'): - test_auth = \ - auth.filter_factory({ - 'super_admin_key': 'supertest', - 's3_support': 'on', - 'auth_type_salt': 'blah', - 'auth_type': atype})(FakeApp()) - self.assertTrue(test_auth.s3_support) - # auth_type_salt need not be set for Plaintext - test_auth = \ - auth.filter_factory({ - 'super_admin_key': 'supertest', - 's3_support': 'on', - 'auth_type': 'Plaintext'})(FakeApp()) - self.assertTrue(test_auth.s3_support) - - def test_s3_disabled_when_conditions_not_met(self): - # Conf says that it wants s3 support but other conditions are not met - # In that case s3 support should be disabled. - for atype in ('Sha1', 'Sha512'): - # auth_type_salt is not set - test_auth = \ - auth.filter_factory({ - 'super_admin_key': 'supertest', - 's3_support': 'on', - 'auth_type': atype})(FakeApp()) - self.assertFalse(test_auth.s3_support) - - def test_s3_authorization_default_off(self): - self.assertFalse(self.test_auth.s3_support) - req = self._make_request('/v1/AUTH_account', environ={ - 'swift3.auth_details': {'unused': 'stuff'}}) - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 400) # HTTPBadRequest - self.assertTrue(resp.environ.get('swift.authorize') is None) - - def test_s3_turned_off_get_groups(self): - env = { - 'swift3.auth_details': {'unused': 'stuff'}} - token = 'whatever' - self.test_auth.logger = mock.Mock() - self.assertEqual(self.test_auth.get_groups(env, token), None) - - def test_default_storage_policy(self): - ath = auth.filter_factory({})(FakeApp()) - self.assertEqual(ath.default_storage_policy, None) - - ath = \ - auth.filter_factory({'default_storage_policy': 'ssd'})(FakeApp()) - self.assertEqual(ath.default_storage_policy, 'ssd') - - def test_s3_creds_unicode_bad(self): - self.test_auth.s3_support = True - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({"auth": unicode("plaintext:key)"), - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_act'}, '')])) - env = \ - {'swift3.auth_details': { - 'access_key': 'act:user', - # NOTE: signature uses password of 'key', not 'key)' - 'signature': '3yW7oFFWOn+fhHMu7E47RKotL1Q=', - 'string_to_sign': base64.urlsafe_b64decode( - 'UFVUCgoKRnJpLCAyNiBGZWIgMjAxNiAwNjo0NT' - 'ozNCArMDAwMAovY29udGFpbmVyMw==')}, - 'PATH_INFO': '/v1/AUTH_act/c1'} - token = 'not used' - self.assertEqual(self.test_auth.get_groups(env, token), None) - - def test_s3_creds_unicode_good(self): - self.test_auth.s3_support = True - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({"auth": unicode("plaintext:key)"), - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_act'}, '')])) - env = \ - {'swift3.auth_details': { - 'access_key': 'act:user', - 'signature': 'dElf49mbXP8t7F+P1qXZzaf3a50=', - 'string_to_sign': base64.urlsafe_b64decode( - 'UFVUCgoKRnJpLCAyNiBGZWIgMjAxNiAwNjo0NT' - 'ozNCArMDAwMAovY29udGFpbmVyMw==')}, - 'PATH_INFO': '/v1/AUTH_act/c1'} - token = 'UFVUCgoKRnJpLCAyNiBGZWIgMjAxNiAwNjo0NT'\ - 'ozNCArMDAwMAovY29udGFpbmVyMw==' - self.assertEqual(self.test_auth.get_groups(env, token), - 'act:usr,act,AUTH_act') - - def test_s3_only_hash_passed_to_hmac(self): - self.test_auth.s3_support = True - key = 'dadada' - salt = 'zuck' - key_hash = hashlib.sha1('%s%s' % (salt, key)).hexdigest() - auth_stored = "sha1:%s$%s" % (salt, key_hash) - self.test_auth.app = FakeApp(iter([ - ('200 Ok', {}, - json.dumps({"auth": auth_stored, - "groups": [{'name': "act:usr"}, {'name': "act"}, - {'name': ".admin"}]})), - ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_act'}, '')])) - env = \ - {'swift3.auth_details': { - 'access_key': 'act:user', - 'signature': 'whatever', - 'string_to_sign': base64.urlsafe_b64decode( - 'UFVUCgoKRnJpLCAyNiBGZWIgMjAxNiAwNjo0NT' - 'ozNCArMDAwMAovY29udGFpbmVyMw==')}, - 'PATH_INFO': '/v1/AUTH_act/c1'} - token = 'not used' - mock_hmac_new = mock.MagicMock() - with mock.patch('hmac.new', mock_hmac_new): - self.test_auth.get_groups(env, token) - self.assertTrue(mock_hmac_new.called) - # Assert that string passed to hmac.new is only the hash - self.assertEqual(mock_hmac_new.call_args[0][0], key_hash) - - def test_get_concealed_token(self): - auth.HASH_PATH_PREFIX = 'start' - auth.HASH_PATH_SUFFIX = 'end' - token = 'token' - - # Check sha512 of "start:token:end" - hashed_token = self.test_auth._get_concealed_token(token) - self.assertEqual(hashed_token, - 'cb320540b0b4c69eb83de2ffb80714cb6766e2d06b5579d1a35a9c4c3fb62' - '981ec50bcc3fb94521133e69a87d1efcb83efd78f35a06b6375e410201476' - '0722f6') - - # Check sha512 of "start:token2:end" - token = 'token2' - hashed_token = self.test_auth._get_concealed_token(token) - self.assertEqual(hashed_token, - 'ca400a6f884c168357f6af0609fda66aecd5aa613147167487495dd9f39fd' - '8a77288568e65857294f01e398d7f14328e855f18517ccf94185d849e7f34' - 'f4259d') - - # Check sha512 of "start2:token2:end" - auth.HASH_PATH_PREFIX = 'start2' - hashed_token = self.test_auth._get_concealed_token(token) - self.assertEqual(hashed_token, - 'ad594a69f44dd6e0aad54e360b01f15bd4833ccb4dcd9116d7aba0c25fb95' - '670155b8cc7175def7aeeb4624a0f2bb7da5f0b204a4680ea7947d3d6a045' - '22bdde') - - # Check sha512 of "start2:token2:end2" - auth.HASH_PATH_SUFFIX = 'end2' - hashed_token = self.test_auth._get_concealed_token(token) - self.assertEqual(hashed_token, - '446af2473ad6b28319a0fe02719a9d715b9941d12e0709851aedb4f53b890' - '693e7f1328e68d870fe114f35f4ed9648b16a5013182db50d3d1f79a660f2' - '0e078e') - - -if __name__ == '__main__': - unittest.main() diff --git a/test/unit/test_swift_version.py b/test/unit/test_swift_version.py deleted file mode 100644 index 5fe278b..0000000 --- a/test/unit/test_swift_version.py +++ /dev/null @@ -1,184 +0,0 @@ -# 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 unittest - -from swauth import swift_version as ver -import swift - - -class TestSwiftVersion(unittest.TestCase): - def test_parse(self): - tests = { - "1.2": (1, 2, 0, True), - "1.2.3": (1, 2, 3, True), - "1.2.3-dev": (1, 2, 3, False) - } - - for (input, ref_out) in tests.items(): - out = ver.parse(input) - self.assertEqual(ref_out, out) - - def test_newer_than(self): - orig_version = swift.__version__ - - swift.__version__ = '1.3' - ver.MAJOR = None - self.assertTrue(ver.newer_than('1.2')) - self.assertTrue(ver.newer_than('1.2.9')) - self.assertTrue(ver.newer_than('1.3-dev')) - self.assertTrue(ver.newer_than('1.3.0-dev')) - self.assertFalse(ver.newer_than('1.3')) - self.assertFalse(ver.newer_than('1.3.0')) - self.assertFalse(ver.newer_than('1.3.1-dev')) - self.assertFalse(ver.newer_than('1.3.1')) - self.assertFalse(ver.newer_than('1.4-dev')) - self.assertFalse(ver.newer_than('1.4')) - self.assertFalse(ver.newer_than('2.0-dev')) - self.assertFalse(ver.newer_than('2.0')) - - swift.__version__ = '1.3-dev' - ver.MAJOR = None - self.assertTrue(ver.newer_than('1.2')) - self.assertTrue(ver.newer_than('1.2.9')) - self.assertFalse(ver.newer_than('1.3-dev')) - self.assertFalse(ver.newer_than('1.3.0-dev')) - self.assertFalse(ver.newer_than('1.3')) - self.assertFalse(ver.newer_than('1.3.0')) - self.assertFalse(ver.newer_than('1.3.1-dev')) - self.assertFalse(ver.newer_than('1.3.1')) - self.assertFalse(ver.newer_than('1.4-dev')) - self.assertFalse(ver.newer_than('1.4')) - self.assertFalse(ver.newer_than('2.0-dev')) - self.assertFalse(ver.newer_than('2.0')) - - swift.__version__ = '1.5.6' - ver.MAJOR = None - self.assertTrue(ver.newer_than('1.4')) - self.assertTrue(ver.newer_than('1.5')) - self.assertTrue(ver.newer_than('1.5.5-dev')) - self.assertTrue(ver.newer_than('1.5.5')) - self.assertTrue(ver.newer_than('1.5.6-dev')) - self.assertFalse(ver.newer_than('1.5.6')) - self.assertFalse(ver.newer_than('1.5.7-dev')) - self.assertFalse(ver.newer_than('1.5.7')) - self.assertFalse(ver.newer_than('1.6-dev')) - self.assertFalse(ver.newer_than('1.6')) - self.assertFalse(ver.newer_than('2.0-dev')) - self.assertFalse(ver.newer_than('2.0')) - - swift.__version__ = '1.5.6-dev' - ver.MAJOR = None - self.assertTrue(ver.newer_than('1.4')) - self.assertTrue(ver.newer_than('1.5')) - self.assertTrue(ver.newer_than('1.5.5-dev')) - self.assertTrue(ver.newer_than('1.5.5')) - self.assertFalse(ver.newer_than('1.5.6-dev')) - self.assertFalse(ver.newer_than('1.5.6')) - self.assertFalse(ver.newer_than('1.5.7-dev')) - self.assertFalse(ver.newer_than('1.5.7')) - self.assertFalse(ver.newer_than('1.6-dev')) - self.assertFalse(ver.newer_than('1.6')) - self.assertFalse(ver.newer_than('2.0-dev')) - self.assertFalse(ver.newer_than('2.0')) - - swift.__version__ = '1.10.0-2.el6' - ver.MAJOR = None - self.assertTrue(ver.newer_than('1.9')) - self.assertTrue(ver.newer_than('1.10.0-dev')) - self.assertFalse(ver.newer_than('1.10.0')) - self.assertFalse(ver.newer_than('1.11')) - self.assertFalse(ver.newer_than('2.0')) - - swift.__version__ = 'garbage' - ver.MAJOR = None - self.assertFalse(ver.newer_than('2.0')) - - swift.__version__ = orig_version - - def test_at_least(self): - orig_version = swift.__version__ - - swift.__version__ = '1.3' - ver.MAJOR = None - self.assertTrue(ver.at_least('1.2')) - self.assertTrue(ver.at_least('1.2.9')) - self.assertTrue(ver.at_least('1.3-dev')) - self.assertTrue(ver.at_least('1.3.0-dev')) - self.assertTrue(ver.at_least('1.3')) - self.assertTrue(ver.at_least('1.3.0')) - self.assertFalse(ver.at_least('1.3.1-dev')) - self.assertFalse(ver.at_least('1.3.1')) - self.assertFalse(ver.at_least('1.4-dev')) - self.assertFalse(ver.at_least('1.4')) - self.assertFalse(ver.at_least('2.0-dev')) - self.assertFalse(ver.at_least('2.0')) - - swift.__version__ = '1.3-dev' - ver.MAJOR = None - self.assertTrue(ver.at_least('1.2')) - self.assertTrue(ver.at_least('1.2.9')) - self.assertTrue(ver.at_least('1.3-dev')) - self.assertTrue(ver.at_least('1.3.0-dev')) - self.assertFalse(ver.at_least('1.3')) - self.assertFalse(ver.at_least('1.3.0')) - self.assertFalse(ver.at_least('1.3.1-dev')) - self.assertFalse(ver.at_least('1.3.1')) - self.assertFalse(ver.at_least('1.4-dev')) - self.assertFalse(ver.at_least('1.4')) - self.assertFalse(ver.at_least('2.0-dev')) - self.assertFalse(ver.at_least('2.0')) - - swift.__version__ = '1.5.6' - ver.MAJOR = None - self.assertTrue(ver.at_least('1.4')) - self.assertTrue(ver.at_least('1.5')) - self.assertTrue(ver.at_least('1.5.5-dev')) - self.assertTrue(ver.at_least('1.5.5')) - self.assertTrue(ver.at_least('1.5.6-dev')) - self.assertTrue(ver.at_least('1.5.6')) - self.assertFalse(ver.at_least('1.5.7-dev')) - self.assertFalse(ver.at_least('1.5.7')) - self.assertFalse(ver.at_least('1.6-dev')) - self.assertFalse(ver.at_least('1.6')) - self.assertFalse(ver.at_least('2.0-dev')) - self.assertFalse(ver.at_least('2.0')) - - swift.__version__ = '1.5.6-dev' - ver.MAJOR = None - self.assertTrue(ver.at_least('1.4')) - self.assertTrue(ver.at_least('1.5')) - self.assertTrue(ver.at_least('1.5.5-dev')) - self.assertTrue(ver.at_least('1.5.5')) - self.assertTrue(ver.at_least('1.5.6-dev')) - self.assertFalse(ver.at_least('1.5.6')) - self.assertFalse(ver.at_least('1.5.7-dev')) - self.assertFalse(ver.at_least('1.5.7')) - self.assertFalse(ver.at_least('1.6-dev')) - self.assertFalse(ver.at_least('1.6')) - self.assertFalse(ver.at_least('2.0-dev')) - self.assertFalse(ver.at_least('2.0')) - - swift.__version__ = '1.10.0-2.el6' - ver.MAJOR = None - self.assertTrue(ver.at_least('1.9')) - self.assertTrue(ver.at_least('1.10.0-dev')) - self.assertTrue(ver.at_least('1.10.0')) - self.assertFalse(ver.at_least('1.11')) - self.assertFalse(ver.at_least('2.0')) - - swift.__version__ = 'garbage' - ver.MAJOR = None - self.assertFalse(ver.at_least('2.0')) - - swift.__version__ = orig_version diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 6d7f101..0000000 --- a/tox.ini +++ /dev/null @@ -1,55 +0,0 @@ -[tox] -minversion = 1.6 -envlist = py27,pep8,cover -skipsdist = True - -[testenv] -basepython = python2.7 -usedevelop = True -install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} -setenv = VIRTUAL_ENV={envdir} - NOSE_WITH_COVERAGE=1 - NOSE_COVER_BRANCHES=1 - NOSE_COVER_ERASE=1 -deps = - -r{toxinidir}/test-requirements.txt - https://tarballs.openstack.org/swift/swift-2.15.1.tar.gz -commands = nosetests {posargs:test/unit} - -[testenv:cover] -setenv = VIRTUAL_ENV={envdir} - NOSE_WITH_COVERAGE=1 - NOSE_COVER_BRANCHES=1 - NOSE_COVER_HTML=1 - NOSE_COVER_HTML_DIR={toxinidir}/cover - NOSE_COVER_MIN_PERCENTAGE=89 - NOSE_COVER_ERASE=1 - -[testenv:pep8] -commands = - flake8 swauth test - flake8 --filename=swauth* bin - bandit -r swauth -s B303,B309 - -[testenv:bandit] -# B303 Use of insecure hash function -# B309 Use of HTTPSConnection -commands = bandit -r swauth -s B303,B309 - -[testenv:venv] -commands = {posargs} - -[testenv:docs] -commands = python setup.py build_sphinx - -[flake8] -# E123 skipped as they are invalid PEP-8. -# will be removed later -# H405 multi line docstring summary not separated with an empty line -# E128 continuation line under-indented for visual indent -# E121 continuation line under-indented for hanging indent - -show-source = True -ignore = E123,H405,E128,E121 -builtins = _ -exclude=.venv,.git,.tox,dist,doc,*egg,build diff --git a/webadmin/index.html b/webadmin/index.html deleted file mode 100644 index 46103ee..0000000 --- a/webadmin/index.html +++ /dev/null @@ -1,575 +0,0 @@ - - - - - - -

-
-
Swauth
-
-
-
- - -