From ac1ac5bd2171e61ba4332dcae33d8a433cef7a4f Mon Sep 17 00:00:00 2001 From: Luis Pabon Date: Thu, 10 Oct 2013 16:53:28 -0400 Subject: GSWauth authentication to be based on SWauth We are planning on creating a GlusterFS aware authentication system for gluster-swift based on SWauth. We forked from SWauth commit 41d36ebe160aa3346f6f45197fff0c80f38fde58 Change-Id: Ia28730d21e04fc8d9ce0cb317fc04d0d97583fca Signed-off-by: Luis Pabon Reviewed-on: http://review.gluster.org/6069 --- gluster/swift/common/middleware/gswauth/.gitignore | 3 + gluster/swift/common/middleware/gswauth/.unittests | 4 + gluster/swift/common/middleware/gswauth/AUTHORS | 39 + gluster/swift/common/middleware/gswauth/CHANGELOG | 62 + gluster/swift/common/middleware/gswauth/LICENSE | 202 ++ .../swift/common/middleware/gswauth/MANIFEST.in | 4 + gluster/swift/common/middleware/gswauth/README.md | 71 + gluster/swift/common/middleware/gswauth/babel.cfg | 2 + .../middleware/gswauth/bin/swauth-add-account | 69 + .../common/middleware/gswauth/bin/swauth-add-user | 99 + .../middleware/gswauth/bin/swauth-cleanup-tokens | 165 + .../middleware/gswauth/bin/swauth-delete-account | 60 + .../middleware/gswauth/bin/swauth-delete-user | 60 + .../common/middleware/gswauth/bin/swauth-list | 86 + .../common/middleware/gswauth/bin/swauth-prep | 59 + .../gswauth/bin/swauth-set-account-service | 73 + .../middleware/gswauth/doc/source/_static/.empty | 0 .../gswauth/doc/source/_templates/.empty | 0 .../common/middleware/gswauth/doc/source/api.rst | 466 +++ .../middleware/gswauth/doc/source/authtypes.rst | 10 + .../common/middleware/gswauth/doc/source/conf.py | 233 ++ .../middleware/gswauth/doc/source/details.rst | 159 + .../common/middleware/gswauth/doc/source/index.rst | 142 + .../middleware/gswauth/doc/source/license.rst | 225 ++ .../middleware/gswauth/doc/source/middleware.rst | 9 + .../middleware/gswauth/doc/source/swauth.rst | 9 + .../gswauth/etc/proxy-server.conf-sample | 78 + .../common/middleware/gswauth/locale/swauth.pot | 30 + gluster/swift/common/middleware/gswauth/setup.cfg | 23 + gluster/swift/common/middleware/gswauth/setup.py | 89 + .../common/middleware/gswauth/swauth/__init__.py | 23 + .../common/middleware/gswauth/swauth/authtypes.py | 103 + .../common/middleware/gswauth/swauth/middleware.py | 1546 +++++++++ .../middleware/gswauth/swauth/swift_version.py | 71 + .../middleware/gswauth/test_swauth/__init__.py | 10 + .../gswauth/test_swauth/unit/__init__.py | 0 .../gswauth/test_swauth/unit/test_authtypes.py | 64 + .../gswauth/test_swauth/unit/test_middleware.py | 3642 ++++++++++++++++++++ .../common/middleware/gswauth/webadmin/index.html | 552 +++ tox.ini | 2 +- 40 files changed, 8543 insertions(+), 1 deletion(-) create mode 100644 gluster/swift/common/middleware/gswauth/.gitignore create mode 100755 gluster/swift/common/middleware/gswauth/.unittests create mode 100644 gluster/swift/common/middleware/gswauth/AUTHORS create mode 100644 gluster/swift/common/middleware/gswauth/CHANGELOG create mode 100644 gluster/swift/common/middleware/gswauth/LICENSE create mode 100644 gluster/swift/common/middleware/gswauth/MANIFEST.in create mode 100644 gluster/swift/common/middleware/gswauth/README.md create mode 100644 gluster/swift/common/middleware/gswauth/babel.cfg create mode 100755 gluster/swift/common/middleware/gswauth/bin/swauth-add-account create mode 100755 gluster/swift/common/middleware/gswauth/bin/swauth-add-user create mode 100755 gluster/swift/common/middleware/gswauth/bin/swauth-cleanup-tokens create mode 100755 gluster/swift/common/middleware/gswauth/bin/swauth-delete-account create mode 100755 gluster/swift/common/middleware/gswauth/bin/swauth-delete-user create mode 100755 gluster/swift/common/middleware/gswauth/bin/swauth-list create mode 100755 gluster/swift/common/middleware/gswauth/bin/swauth-prep create mode 100755 gluster/swift/common/middleware/gswauth/bin/swauth-set-account-service create mode 100644 gluster/swift/common/middleware/gswauth/doc/source/_static/.empty create mode 100644 gluster/swift/common/middleware/gswauth/doc/source/_templates/.empty create mode 100644 gluster/swift/common/middleware/gswauth/doc/source/api.rst create mode 100644 gluster/swift/common/middleware/gswauth/doc/source/authtypes.rst create mode 100644 gluster/swift/common/middleware/gswauth/doc/source/conf.py create mode 100644 gluster/swift/common/middleware/gswauth/doc/source/details.rst create mode 100644 gluster/swift/common/middleware/gswauth/doc/source/index.rst create mode 100644 gluster/swift/common/middleware/gswauth/doc/source/license.rst create mode 100644 gluster/swift/common/middleware/gswauth/doc/source/middleware.rst create mode 100644 gluster/swift/common/middleware/gswauth/doc/source/swauth.rst create mode 100644 gluster/swift/common/middleware/gswauth/etc/proxy-server.conf-sample create mode 100644 gluster/swift/common/middleware/gswauth/locale/swauth.pot create mode 100644 gluster/swift/common/middleware/gswauth/setup.cfg create mode 100644 gluster/swift/common/middleware/gswauth/setup.py create mode 100644 gluster/swift/common/middleware/gswauth/swauth/__init__.py create mode 100644 gluster/swift/common/middleware/gswauth/swauth/authtypes.py create mode 100644 gluster/swift/common/middleware/gswauth/swauth/middleware.py create mode 100644 gluster/swift/common/middleware/gswauth/swauth/swift_version.py create mode 100644 gluster/swift/common/middleware/gswauth/test_swauth/__init__.py create mode 100644 gluster/swift/common/middleware/gswauth/test_swauth/unit/__init__.py create mode 100644 gluster/swift/common/middleware/gswauth/test_swauth/unit/test_authtypes.py create mode 100644 gluster/swift/common/middleware/gswauth/test_swauth/unit/test_middleware.py create mode 100644 gluster/swift/common/middleware/gswauth/webadmin/index.html diff --git a/gluster/swift/common/middleware/gswauth/.gitignore b/gluster/swift/common/middleware/gswauth/.gitignore new file mode 100644 index 0000000..0558c26 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/.gitignore @@ -0,0 +1,3 @@ +*.egg-info +*.py[co] +.DS_Store diff --git a/gluster/swift/common/middleware/gswauth/.unittests b/gluster/swift/common/middleware/gswauth/.unittests new file mode 100755 index 0000000..281ac03 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/.unittests @@ -0,0 +1,4 @@ +#!/bin/bash + +nosetests test_swauth/unit --exe --with-coverage --cover-package swauth --cover-erase +rm -f .coverage diff --git a/gluster/swift/common/middleware/gswauth/AUTHORS b/gluster/swift/common/middleware/gswauth/AUTHORS new file mode 100644 index 0000000..3687625 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/AUTHORS @@ -0,0 +1,39 @@ +Maintainer +---------- +Greg Holt + +Original Authors +---------------- +Chuck Thier +Greg Holt +Greg Lange +Jay Payne +John Dickinson +Michael Barton +Will Reese + +Contributors +------------ +Andrew Clay Shafer +Anne Gentle +Brian K. Jones +Caleb Tennis +Chmouel Boudjnah +Christian Schwede +Chris Wedgwood +Clay Gerrard +Colin Nicholson +Conrad Weidenkeller +Cory Wright +David Goetz +Ed Leafe +Fujita Tomonori +Kapil Thangavelu +Monty Taylor +Pablo Llopis +Paul Jimenez +Pete Zaitcev +Russ Nelson +Scott Simpson +Soren Hansen +Stephen Milton diff --git a/gluster/swift/common/middleware/gswauth/CHANGELOG b/gluster/swift/common/middleware/gswauth/CHANGELOG new file mode 100644 index 0000000..4b85c11 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/CHANGELOG @@ -0,0 +1,62 @@ +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/gluster/swift/common/middleware/gswauth/LICENSE b/gluster/swift/common/middleware/gswauth/LICENSE new file mode 100644 index 0000000..75b5248 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/LICENSE @@ -0,0 +1,202 @@ + + 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/gluster/swift/common/middleware/gswauth/MANIFEST.in b/gluster/swift/common/middleware/gswauth/MANIFEST.in new file mode 100644 index 0000000..c73869e --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/MANIFEST.in @@ -0,0 +1,4 @@ +include AUTHORS LICENSE README.md .unittests test_swauth/__init__.py +include CHANGELOG +graft doc +graft etc diff --git a/gluster/swift/common/middleware/gswauth/README.md b/gluster/swift/common/middleware/gswauth/README.md new file mode 100644 index 0000000..2ccd199 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/README.md @@ -0,0 +1,71 @@ +Swauth +------ + +An Auth Service for Swift as WSGI Middleware that uses Swift itself as a +backing store. Sphinx-built docs at: + +See also for the standard OpenStack +auth service. + + +NOTE +---- + +**Be sure to review the Sphinx-built 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. diff --git a/gluster/swift/common/middleware/gswauth/babel.cfg b/gluster/swift/common/middleware/gswauth/babel.cfg new file mode 100644 index 0000000..15cd6cb --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/babel.cfg @@ -0,0 +1,2 @@ +[python: **.py] + diff --git a/gluster/swift/common/middleware/gswauth/bin/swauth-add-account b/gluster/swift/common/middleware/gswauth/bin/swauth-add-account new file mode 100755 index 0000000..88f8010 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/bin/swauth-add-account @@ -0,0 +1,69 @@ +#!/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. + +import gettext +from optparse import OptionParser +from os.path import basename +from sys import argv, exit + +from swift.common.bufferedhttp import http_connect_raw as http_connect +from swift.common.utils import urlparse + + +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']) + account = args[0] + parsed = urlparse(options.admin_url) + if parsed.scheme not in ('http', 'https'): + raise Exception('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/gluster/swift/common/middleware/gswauth/bin/swauth-add-user b/gluster/swift/common/middleware/gswauth/bin/swauth-add-user new file mode 100755 index 0000000..81eeac7 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/bin/swauth-add-user @@ -0,0 +1,99 @@ +#!/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. + +import gettext +from optparse import OptionParser +from os.path import basename +from sys import argv, exit + +from swift.common.bufferedhttp import http_connect_raw as http_connect +from swift.common.utils import urlparse + + +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('-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']) + account, user, password = args + parsed = urlparse(options.admin_url) + if parsed.scheme not in ('http', 'https'): + raise Exception('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 + 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, + 'X-Auth-User-Key': password, + 'Content-Length': '0'} + if options.admin: + headers['X-Auth-User-Admin'] = 'true' + if options.reseller_admin: + headers['X-Auth-User-Reseller-Admin'] = 'true' + 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/gluster/swift/common/middleware/gswauth/bin/swauth-cleanup-tokens b/gluster/swift/common/middleware/gswauth/bin/swauth-cleanup-tokens new file mode 100755 index 0000000..54bed9d --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/bin/swauth-cleanup-tokens @@ -0,0 +1,165 @@ +#!/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. + +try: + import simplejson as json +except ImportError: + import json +import gettext +import re +from datetime import datetime, timedelta +from optparse import OptionParser +from sys import argv, exit +from time import sleep, time + +from swiftclient.client import Connection, ClientException + +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']) + 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, err: + if err.http_status != 404: + raise + continue + if options.verbose: + print 'Done.' + exit(0) + for x in xrange(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, 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, err: + if err.http_status != 404: + raise + continue + last_modified = datetime(*map(int, re.split('[^\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, 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/gluster/swift/common/middleware/gswauth/bin/swauth-delete-account b/gluster/swift/common/middleware/gswauth/bin/swauth-delete-account new file mode 100755 index 0000000..224e3b3 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/bin/swauth-delete-account @@ -0,0 +1,60 @@ +#!/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. + +import gettext +from optparse import OptionParser +from os.path import basename +from sys import argv, exit + +from swift.common.bufferedhttp import http_connect_raw as http_connect +from swift.common.utils import urlparse + + +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']) + account = args[0] + parsed = urlparse(options.admin_url) + if parsed.scheme not in ('http', 'https'): + raise Exception('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/gluster/swift/common/middleware/gswauth/bin/swauth-delete-user b/gluster/swift/common/middleware/gswauth/bin/swauth-delete-user new file mode 100755 index 0000000..3991d9a --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/bin/swauth-delete-user @@ -0,0 +1,60 @@ +#!/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. + +import gettext +from optparse import OptionParser +from os.path import basename +from sys import argv, exit + +from swift.common.bufferedhttp import http_connect_raw as http_connect +from swift.common.utils import urlparse + + +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']) + account, user = args + parsed = urlparse(options.admin_url) + if parsed.scheme not in ('http', 'https'): + raise Exception('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/gluster/swift/common/middleware/gswauth/bin/swauth-list b/gluster/swift/common/middleware/gswauth/bin/swauth-list new file mode 100755 index 0000000..c49af04 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/bin/swauth-list @@ -0,0 +1,86 @@ +#!/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. + +try: + import simplejson as json +except ImportError: + import json +import gettext +from optparse import OptionParser +from os.path import basename +from sys import argv, exit + +from swift.common.bufferedhttp import http_connect_raw as http_connect +from swift.common.utils import urlparse + + +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']) + parsed = urlparse(options.admin_url) + if parsed.scheme not in ('http', 'https'): + raise Exception('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/gluster/swift/common/middleware/gswauth/bin/swauth-prep b/gluster/swift/common/middleware/gswauth/bin/swauth-prep new file mode 100755 index 0000000..bf2384f --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/bin/swauth-prep @@ -0,0 +1,59 @@ +#!/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. + +import gettext +from optparse import OptionParser +from os.path import basename +from sys import argv, exit + +from swift.common.bufferedhttp import http_connect_raw as http_connect +from swift.common.utils import urlparse + + +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']) + parsed = urlparse(options.admin_url) + if parsed.scheme not in ('http', 'https'): + raise Exception('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/gluster/swift/common/middleware/gswauth/bin/swauth-set-account-service b/gluster/swift/common/middleware/gswauth/bin/swauth-set-account-service new file mode 100755 index 0000000..b0bed38 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/bin/swauth-set-account-service @@ -0,0 +1,73 @@ +#!/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. + +try: + import simplejson as json +except ImportError: + import json +import gettext +from optparse import OptionParser +from os.path import basename +from sys import argv, exit + +from swift.common.bufferedhttp import http_connect_raw as http_connect +from swift.common.utils import urlparse + + +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']) + account, service, name, url = args + parsed = urlparse(options.admin_url) + if parsed.scheme not in ('http', 'https'): + raise Exception('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/gluster/swift/common/middleware/gswauth/doc/source/_static/.empty b/gluster/swift/common/middleware/gswauth/doc/source/_static/.empty new file mode 100644 index 0000000..e69de29 diff --git a/gluster/swift/common/middleware/gswauth/doc/source/_templates/.empty b/gluster/swift/common/middleware/gswauth/doc/source/_templates/.empty new file mode 100644 index 0000000..e69de29 diff --git a/gluster/swift/common/middleware/gswauth/doc/source/api.rst b/gluster/swift/common/middleware/gswauth/doc/source/api.rst new file mode 100644 index 0000000..d2efa0f --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/doc/source/api.rst @@ -0,0 +1,466 @@ +.. _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:: + + 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 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. + +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/gluster/swift/common/middleware/gswauth/doc/source/authtypes.rst b/gluster/swift/common/middleware/gswauth/doc/source/authtypes.rst new file mode 100644 index 0000000..a19ee22 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/doc/source/authtypes.rst @@ -0,0 +1,10 @@ +.. _swauth_authtypes_module: + +swauth.authtypes +================= + +.. automodule:: swauth.authtypes + :members: + :undoc-members: + :show-inheritance: + :noindex: diff --git a/gluster/swift/common/middleware/gswauth/doc/source/conf.py b/gluster/swift/common/middleware/gswauth/doc/source/conf.py new file mode 100644 index 0000000..ab0645a --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/doc/source/conf.py @@ -0,0 +1,233 @@ +# -*- 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. +version = '.'.join(str(v) for v in swauth.version_info[:2]) +# 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/gluster/swift/common/middleware/gswauth/doc/source/details.rst b/gluster/swift/common/middleware/gswauth/doc/source/details.rst new file mode 100644 index 0000000..3b14ad8 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/doc/source/details.rst @@ -0,0 +1,159 @@ +---------------------- +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/gluster/swift/common/middleware/gswauth/doc/source/index.rst b/gluster/swift/common/middleware/gswauth/doc/source/index.rst new file mode 100644 index 0000000..87b22d5 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/doc/source/index.rst @@ -0,0 +1,142 @@ +.. 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. Sphinx-built docs at: http://gholt.github.com/swauth/ + Source available at: https://github.com/gholt/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. + + f. 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. + + +Contents +-------- + +.. toctree:: + :maxdepth: 2 + + license + details + swauth + middleware + api + authtypes + + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/gluster/swift/common/middleware/gswauth/doc/source/license.rst b/gluster/swift/common/middleware/gswauth/doc/source/license.rst new file mode 100644 index 0000000..590a9b4 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/doc/source/license.rst @@ -0,0 +1,225 @@ +.. _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/gluster/swift/common/middleware/gswauth/doc/source/middleware.rst b/gluster/swift/common/middleware/gswauth/doc/source/middleware.rst new file mode 100644 index 0000000..a25acd4 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/doc/source/middleware.rst @@ -0,0 +1,9 @@ +.. _swauth_middleware_module: + +swauth.middleware +================= + +.. automodule:: swauth.middleware + :members: + :undoc-members: + :show-inheritance: diff --git a/gluster/swift/common/middleware/gswauth/doc/source/swauth.rst b/gluster/swift/common/middleware/gswauth/doc/source/swauth.rst new file mode 100644 index 0000000..c50c350 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/doc/source/swauth.rst @@ -0,0 +1,9 @@ +.. _swauth_module: + +swauth +====== + +.. automodule:: swauth + :members: + :undoc-members: + :show-inheritance: diff --git a/gluster/swift/common/middleware/gswauth/etc/proxy-server.conf-sample b/gluster/swift/common/middleware/gswauth/etc/proxy-server.conf-sample new file mode 100644 index 0000000..a5f4ea1 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/etc/proxy-server.conf-sample @@ -0,0 +1,78 @@ +[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 +# 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 'sha1' 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 another method that can make use of a salt. +# auth_type_salt = swauthsalt +# 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 +# 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/gluster/swift/common/middleware/gswauth/locale/swauth.pot b/gluster/swift/common/middleware/gswauth/locale/swauth.pot new file mode 100644 index 0000000..86bcbec --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/locale/swauth.pot @@ -0,0 +1,30 @@ +# Translations template for swauth. +# Copyright (C) 2011 ORGANIZATION +# This file is distributed under the same license as the swauth project. +# FIRST AUTHOR , 2011. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: swauth 1.0.1.dev\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2011-05-26 10:35+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.4\n" + +#: swauth/middleware.py:94 +msgid "No super_admin_key set in conf file! Exiting." +msgstr "" + +#: swauth/middleware.py:637 +#, python-format +msgid "" +"ERROR: Exception while trying to communicate with " +"%(scheme)s://%(host)s:%(port)s/%(path)s" +msgstr "" + diff --git a/gluster/swift/common/middleware/gswauth/setup.cfg b/gluster/swift/common/middleware/gswauth/setup.cfg new file mode 100644 index 0000000..a0122b1 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/setup.cfg @@ -0,0 +1,23 @@ +[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 = locale +domain = swauth + +[update_catalog] +domain = swauth +output_dir = locale +input_file = locale/swauth.pot + +[extract_messages] +keywords = _ l_ lazy_gettext +mapping_file = babel.cfg +output_file = locale/swauth.pot diff --git a/gluster/swift/common/middleware/gswauth/setup.py b/gluster/swift/common/middleware/gswauth/setup.py new file mode 100644 index 0000000..ceef1ca --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/setup.py @@ -0,0 +1,89 @@ +#!/usr/bin/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 setuptools import setup, find_packages +from setuptools.command.sdist import sdist +import os +import subprocess +try: + from babel.messages import frontend +except ImportError: + frontend = None + +from swauth import __version__ as version + + +class local_sdist(sdist): + """Customized sdist hook - builds the ChangeLog file from VC first""" + + def run(self): + if os.path.isdir('.bzr'): + # We're in a bzr branch + + log_cmd = subprocess.Popen(["bzr", "log", "--gnu"], + stdout=subprocess.PIPE) + changelog = log_cmd.communicate()[0] + with open("ChangeLog", "w") as changelog_file: + changelog_file.write(changelog) + sdist.run(self) + + +name = 'swauth' + + +cmdclass = {'sdist': local_sdist} + + +if frontend: + cmdclass.update({ + 'compile_catalog': frontend.compile_catalog, + 'extract_messages': frontend.extract_messages, + 'init_catalog': frontend.init_catalog, + 'update_catalog': frontend.update_catalog, + }) + + +setup( + name=name, + version=version, + description='Swauth', + license='Apache License (2.0)', + author='OpenStack, LLC.', + author_email='swauth@brim.net', + url='https://github.com/gholt/swauth', + packages=find_packages(exclude=['test_swauth', 'bin']), + test_suite='nose.collector', + cmdclass=cmdclass, + classifiers=[ + 'Development Status :: 4 - Beta', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python :: 2.6', + 'Environment :: No Input/Output (Daemon)', + ], + install_requires=[], # removed for better compat + 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', + ], + }, + ) diff --git a/gluster/swift/common/middleware/gswauth/swauth/__init__.py b/gluster/swift/common/middleware/gswauth/swauth/__init__.py new file mode 100644 index 0000000..67f86b6 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/swauth/__init__.py @@ -0,0 +1,23 @@ +# 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 + + +#: Version information (major, minor, revision[, 'dev']). +version_info = (1, 0, 9, 'dev') +#: Version string 'major.minor.revision'. +version = __version__ = ".".join(map(str, version_info)) +gettext.install('swauth') diff --git a/gluster/swift/common/middleware/gswauth/swauth/authtypes.py b/gluster/swift/common/middleware/gswauth/swauth/authtypes.py new file mode 100644 index 0000000..90aad72 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/swauth/authtypes.py @@ -0,0 +1,103 @@ +# 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. + +Note that, since some of the encodings will be hashes, swauth supports the +notion of salts. Thus, self.salt will be set to either a user-specified salt +value or to a default value. +""" + +import hashlib + + +#: Maximum length any valid token should ever be. +MAX_TOKEN_LENGTH = 5000 + + +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): + """ + Checks whether the user-provided key matches the user's credentials + + :param key: User-supplied key + :param creds: User's stored credentials + :returns: True if the supplied key is valid, False otherwise + """ + return self.encode(key) == creds + + +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(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 + """ + enc_key = '%s%s' % (self.salt, key) + enc_val = hashlib.sha1(enc_key).hexdigest() + return "sha1:%s$%s" % (self.salt, enc_val) + + def match(self, key, creds): + """ + Checks whether the user-provided key matches the user's credentials + + :param key: User-supplied key + :param creds: User's stored credentials + :returns: True if the supplied key is valid, False otherwise + """ + return self.encode(key) == creds diff --git a/gluster/swift/common/middleware/gswauth/swauth/middleware.py b/gluster/swift/common/middleware/gswauth/swauth/middleware.py new file mode 100644 index 0000000..495bea8 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/swauth/middleware.py @@ -0,0 +1,1546 @@ +# 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. + +try: + import simplejson as json +except ImportError: + import json +from httplib import HTTPConnection, HTTPSConnection +from time import gmtime, strftime, time +from traceback import format_exc +from urllib import quote, unquote +from uuid import uuid4 +from hashlib import md5, sha1 +import hmac +import base64 + +from eventlet.timeout import Timeout +from eventlet import TimeoutError +from swift.common.swob import HTTPAccepted, HTTPBadRequest, HTTPConflict, \ + HTTPCreated, HTTPForbidden, HTTPMethodNotAllowed, HTTPMovedPermanently, \ + HTTPNoContent, HTTPNotFound, HTTPServiceUnavailable, HTTPUnauthorized, \ + Request, Response + +from swift.common.bufferedhttp import http_connect_raw as http_connect +from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed +from swift.common.utils import cache_from_env, get_logger, get_remote_client, \ + split_path, TRUE_VALUES, urlparse +import swift.common.wsgi + +from swauth import swift_version +import swauth.authtypes + + +MEMCACHE_TIME = swift_version.newer_than('1.7.7-dev') + + +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') + 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.') + try: + self.logger.critical(msg) + except Exception: + pass + 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 %s for url %s!') % \ + (self.swauth_remote_parsed.scheme, repr(self.swauth_remote)) + try: + self.logger.critical(msg) + except Exception: + pass + 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 Exception('Invalid cluster format') + self.dsc_parsed = urlparse(self.dsc_url) + if self.dsc_parsed.scheme not in ('http', 'https'): + raise Exception('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 Exception('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.') + try: + self.logger.warn(msg) + except Exception: + pass + 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 Exception('Invalid auth_type in config file: %s' + % self.auth_type) + self.auth_encoder.salt = conf.get('auth_type_salt', 'swauthsalt') + self.allow_overrides = \ + conf.get('allow_overrides', 't').lower() in TRUE_VALUES + self.agent = '%(orig)s Swauth' + self.swift_source = 'SWTH' + + 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. + """ + 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. + """ + # 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('HTTP_AUTHORIZATION') + 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: + version, rest = None, 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_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 + + if env.get('HTTP_AUTHORIZATION'): + if self.swauth_remote: + # TODO: Support S3-style authorization with swauth_remote mode + self.logger.warn('S3-style authorization not supported yet ' + 'with swauth_remote mode.') + return None + try: + account = env['HTTP_AUTHORIZATION'].split(' ')[1] + account, user, sign = account.split(':') + except Exception, err: + self.logger.debug( + 'Swauth cannot parse Authorization header value %r' % + env['HTTP_AUTHORIZATION']) + 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) + + password = detail['auth'].split(':')[-1] + msg = base64.urlsafe_b64decode(unquote(token)) + s = base64.encodestring(hmac.new(password, + msg, sha1).digest()).strip() + if s != sign: + 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: + if MEMCACHE_TIME: + memcache_client.set( + memcache_key, (time() + expires_from_now, groups), + time=expires_from_now) + else: + memcache_client.set( + memcache_key, (time() + expires_from_now, groups), + timeout=expires_from_now) + else: + path = quote('/v1/%s/.token_%s/%s' % + (self.auth_account, token[-1], token)) + 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: + if MEMCACHE_TIME: + memcache_client.set( + memcache_key, + (detail['expires'], groups), + time=float(detail['expires'] - time())) + else: + memcache_client.set( + memcache_key, + (detail['expires'], groups), + timeout=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 req.remote_user: + 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 HTTPForbidden(request=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 HTTPForbidden(request=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})) + + 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 HTTPForbidden(request=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(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 HTTPForbidden(request=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, 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) + + 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 HTTPForbidden(request=req) + account = req.path_info_pop() + if req.path_info or not account or account[0] == '.': + return HTTPBadRequest(request=req) + # 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)) + 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 + # 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 HTTPForbidden(request=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 HTTPForbidden(request=req) + if user == '.groups': + # TODO: 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] != '.': + path = quote('/v1/%s/%s/%s' % (self.auth_account, + account, obj['name'])) + resp = self.make_pre_authed_request( + req.environ, 'GET', path).get_response(self.app) + if resp.status_int // 100 != 2: + raise Exception('Could not retrieve user object: ' + '%s %s' % (path, resp.status)) + groups.update(g['name'] + for g in json.loads(resp.body)['groups']) + marker = sublisting[-1]['name'].encode('utf-8') + body = json.dumps({'groups': + [{'name': g} for g in sorted(groups)]}) + else: + 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 HTTPNotFound(request=req) + if resp.status_int // 100 != 2: + raise Exception('Could not retrieve user object: %s %s' % + (path, resp.status)) + body = resp.body + 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 HTTPForbidden(request=req) + return Response(body=body) + + 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), + 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. + + 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. + + :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', '')) + 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: + return HTTPBadRequest(request=req) + if reseller_admin: + if not self.is_super_admin(req): + return HTTPForbidden(request=req) + elif not self.is_account_admin(req, account): + return HTTPForbidden(request=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 = 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 not self.is_account_admin(req, account): + return HTTPForbidden(request=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: + path = quote('/v1/%s/.token_%s/%s' % + (self.auth_account, candidate_token[-1], candidate_token)) + 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 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, + 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: + path = quote('/v1/%s/.token_%s/%s' % + (self.auth_account, candidate_token[-1], candidate_token)) + 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) + # 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 + path = quote('/v1/%s/.token_%s/%s' % + (self.auth_account, token[-1], token)) + 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, + 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: + path = quote('/v1/%s/.token_%s/%s' % + (self.auth_account, token[-1], token)) + 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 - 60 + memcache_client = cache_from_env(env) + if not memcache_client: + raise Exception( + 'No memcache set up; required for Swauth middleware') + if MEMCACHE_TIME: + memcache_client.set( + memcache_key, + (self.itoken_expires, + '.auth,.reseller_admin,%s.auth' % self.reseller_prefix), + time=self.token_life) + else: + memcache_client.set( + memcache_key, + (self.itoken_expires, + '.auth,.reseller_admin,%s.auth' % self.reseller_prefix), + timeout=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) + path = quote('/v1/%s/%s/%s' % (self.auth_account, admin_account, + admin_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 admin user object: %s %s' % + (path, resp.status)) + admin_detail = json.loads(resp.body) + admin_detail['account'] = admin_account + return admin_detail + + def credentials_match(self, user_detail, key): + """ + Returns True if the key is valid for the user_detail. + It will use self.auth_encoder 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. + """ + return user_detail and self.auth_encoder().match( + key, user_detail.get('auth')) + + 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. + """ + 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 + 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. + """ + 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 + 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/gluster/swift/common/middleware/gswauth/swauth/swift_version.py b/gluster/swift/common/middleware/gswauth/swauth/swift_version.py new file mode 100644 index 0000000..cabe284 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/swauth/swift_version.py @@ -0,0 +1,71 @@ +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)) + else: + revision = 0 + return major, minor, revision, final + + +def newer_than(value): + global MAJOR, MINOR, REVISION, FINAL + 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 + return True + + +def run_tests(): + global MAJOR, MINOR, REVISION, FINAL + MAJOR, MINOR, REVISION, FINAL = parse('1.3') + assert(newer_than('1.2')) + assert(newer_than('1.2.9')) + assert(newer_than('1.3-dev')) + assert(newer_than('1.3.0-dev')) + assert(not newer_than('1.3')) + assert(not newer_than('1.3.0')) + assert(not newer_than('1.3.1-dev')) + assert(not newer_than('1.3.1')) + assert(not newer_than('1.4')) + assert(not newer_than('2.0')) + MAJOR, MINOR, REVISION, FINAL = parse('1.7.7-dev') + assert(newer_than('1.6')) + assert(newer_than('1.7')) + assert(newer_than('1.7.6-dev')) + assert(newer_than('1.7.6')) + assert(not newer_than('1.7.7')) + assert(not newer_than('1.7.8-dev')) + assert(not newer_than('1.7.8')) + assert(not newer_than('1.8.0')) + assert(not newer_than('2.0')) + + +if __name__ == '__main__': + run_tests() diff --git a/gluster/swift/common/middleware/gswauth/test_swauth/__init__.py b/gluster/swift/common/middleware/gswauth/test_swauth/__init__.py new file mode 100644 index 0000000..0ee3666 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/test_swauth/__init__.py @@ -0,0 +1,10 @@ +# See http://code.google.com/p/python-nose/issues/detail?id=373 +# The code below enables nosetests to work with i18n _() blocks + +import __builtin__ +import sys +import os +from ConfigParser import MissingSectionHeaderError +from StringIO import StringIO + +setattr(__builtin__, '_', lambda x: x) diff --git a/gluster/swift/common/middleware/gswauth/test_swauth/unit/__init__.py b/gluster/swift/common/middleware/gswauth/test_swauth/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gluster/swift/common/middleware/gswauth/test_swauth/unit/test_authtypes.py b/gluster/swift/common/middleware/gswauth/test_swauth/unit/test_authtypes.py new file mode 100644 index 0000000..eda1de4 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/test_swauth/unit/test_authtypes.py @@ -0,0 +1,64 @@ +# 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 unittest +from contextlib import contextmanager +from swauth import authtypes + + +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.assertEquals('plaintext:keystring', enc_key) + + def test_plaintext_valid_match(self): + creds = 'plaintext:keystring' + match = self.auth_encoder.match('keystring', creds) + self.assertEquals(match, True) + + def test_plaintext_invalid_match(self): + creds = 'plaintext:other-keystring' + match = self.auth_encoder.match('keystring', creds) + self.assertEquals(match, False) + + +class TestSha1(unittest.TestCase): + + def setUp(self): + self.auth_encoder = authtypes.Sha1() + self.auth_encoder.salt = 'salt' + + def test_sha1_encode(self): + enc_key = self.auth_encoder.encode('keystring') + self.assertEquals('sha1:salt$d50dc700c296e23ce5b41f7431a0e01f69010f06', + enc_key) + + def test_sha1_valid_match(self): + creds = 'sha1:salt$d50dc700c296e23ce5b41f7431a0e01f69010f06' + match = self.auth_encoder.match('keystring', creds) + self.assertEquals(match, True) + + def test_sha1_invalid_match(self): + creds = 'sha1:salt$deadbabedeadbabedeadbabec0ffeebadc0ffeee' + match = self.auth_encoder.match('keystring', creds) + self.assertEquals(match, False) + + +if __name__ == '__main__': + unittest.main() diff --git a/gluster/swift/common/middleware/gswauth/test_swauth/unit/test_middleware.py b/gluster/swift/common/middleware/gswauth/test_swauth/unit/test_middleware.py new file mode 100644 index 0000000..9029f89 --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/test_swauth/unit/test_middleware.py @@ -0,0 +1,3642 @@ +# 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. + +try: + import simplejson as json +except ImportError: + import json +import unittest +from contextlib import contextmanager +from time import time + +from swift.common.swob import Request, Response + +from swauth import middleware as auth +from swauth.authtypes import MAX_TOKEN_LENGTH + + +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, timeout=0, time=0): + self.store[key] = value + return True + + def incr(self, key, timeout=0, time=0): + self.store[key] = self.store.setdefault(key, 0) + 1 + return self.store[key] + + @contextmanager + def soft_lock(self, key, timeout=0, 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_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.assertEquals(ath.reseller_prefix, 'AUTH_') + ath = auth.filter_factory({'super_admin_key': 'supertest', + 'reseller_prefix': 'TEST'})(app) + self.assertEquals(ath.reseller_prefix, 'TEST_') + ath = auth.filter_factory({'super_admin_key': 'supertest', + 'reseller_prefix': 'TEST_'})(app) + self.assertEquals(ath.reseller_prefix, 'TEST_') + + def test_auth_prefix_init(self): + app = FakeApp() + ath = auth.filter_factory({'super_admin_key': 'supertest'})(app) + self.assertEquals(ath.auth_prefix, '/auth/') + ath = auth.filter_factory({'super_admin_key': 'supertest', + 'auth_prefix': ''})(app) + self.assertEquals(ath.auth_prefix, '/auth/') + ath = auth.filter_factory({'super_admin_key': 'supertest', + 'auth_prefix': '/test/'})(app) + self.assertEquals(ath.auth_prefix, '/test/') + ath = auth.filter_factory({'super_admin_key': 'supertest', + 'auth_prefix': '/test'})(app) + self.assertEquals(ath.auth_prefix, '/test/') + ath = auth.filter_factory({'super_admin_key': 'supertest', + 'auth_prefix': 'test/'})(app) + self.assertEquals(ath.auth_prefix, '/test/') + ath = auth.filter_factory({'super_admin_key': 'supertest', + 'auth_prefix': 'test'})(app) + self.assertEquals(ath.auth_prefix, '/test/') + + def test_no_auth_type_init(self): + app = FakeApp() + ath = auth.filter_factory({})(app) + self.assertEquals(ath.auth_type, 'Plaintext') + + def test_valid_auth_type_init(self): + app = FakeApp() + ath = auth.filter_factory({'auth_type': 'sha1'})(app) + self.assertEquals(ath.auth_type, 'Sha1') + ath = auth.filter_factory({'auth_type': 'plaintext'})(app) + self.assertEquals(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.assertEquals(str(exc), + 'Invalid auth_type in config file: %s' % + 'Nonexistant') + + def test_default_swift_cluster_init(self): + app = FakeApp() + self.assertRaises(Exception, 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.assertEquals(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.assertEquals(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.assertEquals(ath.dsc_url, 'https://host/path') + self.assertEquals(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.assertEquals(ath.dsc_url, 'https://host/path') + self.assertEquals(ath.dsc_url2, 'http://host2/path2') + + def test_top_level_denied(self): + resp = Request.blank('/').get_response(self.test_auth) + self.assertEquals(resp.status_int, 401) + + def test_anon(self): + resp = Request.blank('/v1/AUTH_account').get_response(self.test_auth) + self.assertEquals(resp.status_int, 401) + self.assertEquals(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.assertEquals(resp.status_int, 401) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 401) + # one for checking auth, two for request passed along + self.assertEquals(local_app.calls, 2) + self.assertEquals(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.assertEquals(resp.status_int, 204) + self.assertEquals(local_app.calls, 2) + self.assertEquals(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.assertEquals(resp.status_int, 401) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 204) + self.assertEquals(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.assertEquals(resp.status_int, 204) + resp = Request.blank('/v1/AUTH_cfa', + headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth) + self.assertEquals(resp.status_int, 204) + self.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 204) + self.assertEquals(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.assertEquals(resp.status_int, 401) + self.assertEquals(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.assertEquals(resp.status_int, 204) + self.assertEquals(self.test_auth.app.calls, 2) + + def test_authorize_bad_path(self): + req = Request.blank('/badpath') + resp = self.test_auth.authorize(req) + self.assertEquals(resp.status_int, 401) + req = Request.blank('/badpath') + req.remote_user = 'act:usr,act,AUTH_cfa' + resp = self.test_auth.authorize(req) + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 403) + req = Request.blank('/v1/AUTH_cfa') + req.remote_user = 'act:usr,act' + req.acl = 'act' + self.assertEquals(self.test_auth.authorize(req), None) + req = Request.blank('/v1/AUTH_cfa') + req.remote_user = 'act:usr,act' + req.acl = 'act:usr' + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 403) + req = Request.blank('/v1/AUTH_cfa/c') + req.remote_user = 'act:usr,act' + req.acl = '.r:*,.rlistings' + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(self.test_auth.authorize(req), None) + req = Request.blank('/v1/AUTH_cfa/c') + resp = self.test_auth.authorize(req) + self.assertEquals(resp.status_int, 401) + req = Request.blank('/v1/AUTH_cfa/c') + req.acl = '.r:*,.rlistings' + self.assertEquals(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.assertEquals(resp.status_int, 401) + req = Request.blank('/v1/AUTH_cfa/c') + req.acl = '.r:.example.com,.rlistings' + resp = self.test_auth.authorize(req) + self.assertEquals(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.assertEquals(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) + resp = 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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 403) + + def test_get_token_fail(self): + resp = Request.blank('/auth/v1.0').get_response(self.test_auth) + self.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 401) + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assert_(resp.headers.get('x-auth-token', + '').startswith('AUTH_tk'), resp.headers.get('x-auth-token')) + self.assertEquals(resp.headers.get('x-auth-token'), + resp.headers.get('x-storage-token')) + self.assertEquals(resp.headers.get('x-storage-url'), + 'http://127.0.0.1:8080/v1/AUTH_cfa') + self.assertEquals(json.loads(resp.body), + {"storage": {"default": "local", + "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) + self.assertEquals(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.assertEquals(resp.status_int, 200) + left = int(resp.headers['x-auth-token-expires']) + self.assertTrue(left > 0, '%d > 0' % left) + self.assertTrue(left <= 10, '%d <= 10' % left) + self.assert_(resp.headers.get('x-auth-token', + '').startswith('AUTH_tk'), resp.headers.get('x-auth-token')) + self.assertEquals(resp.headers.get('x-auth-token'), + resp.headers.get('x-storage-token')) + self.assertEquals(resp.headers.get('x-storage-url'), + 'http://127.0.0.1:8080/v1/AUTH_cfa') + self.assertEquals(json.loads(resp.body), + {"storage": {"default": "local", + "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) + self.assertEquals(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.assertEquals(resp.status_int, 200) + 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.assert_(resp.headers.get('x-auth-token', + '').startswith('AUTH_tk'), resp.headers.get('x-auth-token')) + self.assertEquals(resp.headers.get('x-auth-token'), + resp.headers.get('x-storage-token')) + self.assertEquals(resp.headers.get('x-storage-url'), + 'http://127.0.0.1:8080/v1/AUTH_cfa') + self.assertEquals(json.loads(resp.body), + {"storage": {"default": "local", + "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assert_(resp.headers.get('x-auth-token', + '').startswith('AUTH_tk'), resp.headers.get('x-auth-token')) + self.assertEquals(resp.headers.get('x-auth-token'), + resp.headers.get('x-storage-token')) + self.assertEquals(resp.headers.get('x-storage-url'), + 'http://127.0.0.1:8080/v1/AUTH_cfa') + self.assertEquals(json.loads(resp.body), + {"storage": {"default": "local", + "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assert_(resp.headers.get('x-auth-token', + '').startswith('AUTH_tk'), resp.headers.get('x-auth-token')) + self.assertEquals(resp.headers.get('x-auth-token'), + resp.headers.get('x-storage-token')) + self.assertEquals(resp.headers.get('x-storage-url'), + 'http://127.0.0.1:8080/v1/AUTH_cfa') + self.assertEquals(json.loads(resp.body), + {"storage": {"default": "local", + "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assert_(resp.headers.get('x-auth-token', + '').startswith('AUTH_tk'), resp.headers.get('x-auth-token')) + self.assertEquals(resp.headers.get('x-auth-token'), + resp.headers.get('x-storage-token')) + self.assertEquals(resp.headers.get('x-storage-url'), + 'http://127.0.0.1:8080/v1/AUTH_cfa') + self.assertEquals(json.loads(resp.body), + {"storage": {"default": "local", + "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertEquals(resp.headers.get('x-auth-token'), 'AUTH_tktest') + self.assertEquals(resp.headers.get('x-auth-token'), + resp.headers.get('x-storage-token')) + self.assertEquals(resp.headers.get('x-storage-url'), + 'http://127.0.0.1:8080/v1/AUTH_cfa') + self.assertEquals(json.loads(resp.body), + {"storage": {"default": "local", + "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertNotEquals(resp.headers.get('x-auth-token'), 'AUTH_tktest') + self.assertEquals(resp.headers.get('x-auth-token'), + resp.headers.get('x-storage-token')) + self.assertEquals(resp.headers.get('x-storage-url'), + 'http://127.0.0.1:8080/v1/AUTH_cfa') + self.assertEquals(json.loads(resp.body), + {"storage": {"default": "local", + "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertNotEquals(resp.headers.get('x-auth-token'), 'AUTH_tktest') + self.assertEquals(resp.headers.get('x-auth-token'), + resp.headers.get('x-storage-token')) + self.assertEquals(resp.headers.get('x-storage-url'), + 'http://127.0.0.1:8080/v1/AUTH_cfa') + self.assertEquals(json.loads(resp.body), + {"storage": {"default": "local", + "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertNotEquals(resp.headers.get('x-auth-token'), 'AUTH_tktest') + self.assertEquals(resp.headers.get('x-auth-token'), + resp.headers.get('x-storage-token')) + self.assertEquals(resp.headers.get('x-storage-url'), + 'http://127.0.0.1:8080/v1/AUTH_cfa') + self.assertEquals(json.loads(resp.body), + {"storage": {"default": "local", + "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) + self.assertEquals(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.assertEquals(resp.status_int, 204) + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 403) + 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.assertEquals(resp.status_int, 403) + resp = Request.blank('/auth/v2/.prep', + environ={'REQUEST_METHOD': 'POST'}, + headers={'X-Auth-Admin-User': '.super_admin'} + ).get_response(self.test_auth) + self.assertEquals(resp.status_int, 403) + resp = Request.blank('/auth/v2/.prep', + environ={'REQUEST_METHOD': 'POST'}, + headers={'X-Auth-Admin-Key': 'supertest'} + ).get_response(self.test_auth) + self.assertEquals(resp.status_int, 403) + resp = Request.blank('/auth/v2/.prep', + environ={'REQUEST_METHOD': 'POST'}).get_response(self.test_auth) + self.assertEquals(resp.status_int, 403) + + 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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertEquals(json.loads(resp.body), + {"accounts": [{"name": "act"}]}) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertEquals(json.loads(resp.body), + {"accounts": [{"name": "act"}]}) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 404) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 404) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertEquals(json.loads(resp.body), + {'storage': {'default': 'local', + 'local': 'http://127.0.0.1:8080/v1/AUTH_cfa'}, + 'new_service': {'new_endpoint': 'new_value'}}) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertEquals(json.loads(resp.body), + {'storage': {'default': 'local', + 'local': 'http://127.0.0.1:8080/v1/AUTH_cfa', + 'new_endpoint': 'new_value'}}) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertEquals(json.loads(resp.body), + {'storage': {'default': 'local', + 'local': 'new_value'}}) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 404) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 201) + self.assertEquals(self.test_auth.app.calls, 5) + self.assertEquals(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.assertEquals(resp.status_int, 201) + self.assertEquals(self.test_auth.app.calls, 4) + self.assertEquals(conn.calls, 1) + + def test_put_account_success_preexist_and_completed(self): + 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.assertEquals(resp.status_int, 202) + self.assertEquals(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.assertEquals(resp.status_int, 201) + self.assertEquals(conn.request_path, '/v1/AUTH_test-suffix') + self.assertEquals(self.test_auth.app.calls, 5) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 400) + + def test_put_account_fail_on_initial_account_head(self): + 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.assertEquals(resp.status_int, 500) + self.assertEquals(self.test_auth.app.calls, 1) + + def test_put_account_fail_on_account_marker_put(self): + 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.assertEquals(resp.status_int, 500) + self.assertEquals(self.test_auth.app.calls, 2) + + 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([ + # Initial HEAD of account container to check for pre-existence + ('404 Not Found', {}, ''), + # PUT of account container + ('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.assertEquals(resp.status_int, 500) + self.assertEquals(conn.calls, 1) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(conn.calls, 1) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(conn.calls, 1) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(conn.calls, 1) + self.assertEquals(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.assertEquals(resp.status_int, 204) + self.assertEquals(self.test_auth.app.calls, 6) + self.assertEquals(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.assertEquals(resp.status_int, 204) + self.assertEquals(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.assertEquals(resp.status_int, 204) + self.assertEquals(self.test_auth.app.calls, 6) + self.assertEquals(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.assertEquals(resp.status_int, 204) + self.assertEquals(self.test_auth.app.calls, 6) + self.assertEquals(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.assertEquals(resp.status_int, 204) + self.assertEquals(self.test_auth.app.calls, 6) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 404) + self.assertEquals(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.assertEquals(resp.status_int, 404) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 409) + self.assertEquals(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.assertEquals(resp.status_int, 409) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 409) + self.assertEquals(self.test_auth.app.calls, 3) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(self.test_auth.app.calls, 3) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(self.test_auth.app.calls, 3) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(self.test_auth.app.calls, 3) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(self.test_auth.app.calls, 4) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(self.test_auth.app.calls, 5) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(self.test_auth.app.calls, 6) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertEquals(resp.body, json.dumps( + {"groups": [{"name": "act:usr"}, {"name": "act"}, + {"name": ".admin"}], + "auth": "plaintext:key"})) + self.assertEquals(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.assertEquals(resp.status_int, 404) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertEquals(resp.body, json.dumps( + {"groups": [{"name": ".admin"}, {"name": "act"}, + {"name": "act:tester"}, {"name": "act:tester3"}]})) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertEquals(resp.body, json.dumps( + {"groups": [{"name": ".admin"}, {"name": "act"}, + {"name": "act:tester"}, {"name": "act:tester3"}]})) + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertEquals(resp.body, json.dumps( + {"groups": [{"name": "act:usr"}, {"name": "act"}], + "auth": "plaintext:key"})) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 403) + self.assertEquals(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.assertEquals(resp.status_int, 200) + self.assertEquals(resp.body, json.dumps( + {"groups": [{"name": "act:usr"}, {"name": "act"}, + {"name": ".reseller_admin"}], + "auth": "plaintext:key"})) + self.assertEquals(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.assertEquals(resp.status_int, 404) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(resp.status_int, 404) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 400) + + def test_put_user_reseller_admin_fail_bad_creds(self): + self.test_auth.app = FakeApp(iter([ + # 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.assertEquals(resp.status_int, 403) + self.assertEquals(self.test_auth.app.calls, 0) + + self.test_auth.app = FakeApp(iter([ + # 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.assertEquals(resp.status_int, 403) + self.assertEquals(self.test_auth.app.calls, 0) + + self.test_auth.app = FakeApp(iter([ + # 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.assertEquals(resp.status_int, 403) + self.assertEquals(self.test_auth.app.calls, 0) + + 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"}))])) + 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.assertEquals(resp.status_int, 403) + self.assertEquals(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', + 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.assertEquals(resp.status_int, 403) + self.assertEquals(self.test_auth.app.calls, 1) + + 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"}))])) + 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.assertEquals(resp.status_int, 403) + self.assertEquals(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', + 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.assertEquals(resp.status_int, 403) + self.assertEquals(self.test_auth.app.calls, 1) + + 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.assertEquals(resp.status_int, 201) + self.assertEquals(self.test_auth.app.calls, 2) + self.assertEquals(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.assertEquals(resp.status_int, 201) + self.assertEquals(self.test_auth.app.calls, 2) + self.assertEquals(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.assertEquals(resp.status_int, 201) + self.assertEquals(self.test_auth.app.calls, 2) + self.assertEquals(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.assertEquals(resp.status_int, 201) + self.assertEquals(self.test_auth.app.calls, 2) + self.assertEquals(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.assertEquals(resp.status_int, 404) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(self.test_auth.app.calls, 1) + + def test_delete_user_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"}))])) + 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.assertEquals(resp.status_int, 403) + self.assertEquals(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', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'X-Auth-Admin-User': 'act:usr', + 'X-Auth-Admin-Key': 'key'} + ).get_response(self.test_auth) + self.assertEquals(resp.status_int, 403) + self.assertEquals(self.test_auth.app.calls, 1) + + 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.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 404) + self.assertEquals(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.assertEquals(resp.status_int, 500) + self.assertEquals(self.test_auth.app.calls, 1) + + def test_delete_user_fail_delete_token(self): + self.test_auth.app = FakeApp(iter([ + # 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.assertEquals(resp.status_int, 500) + self.assertEquals(self.test_auth.app.calls, 2) + + def test_delete_user_fail_delete_user(self): + self.test_auth.app = FakeApp(iter([ + # 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.assertEquals(resp.status_int, 500) + self.assertEquals(self.test_auth.app.calls, 3) + + def test_delete_user_success(self): + self.test_auth.app = FakeApp(iter([ + # 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.assertEquals(resp.status_int, 204) + self.assertEquals(self.test_auth.app.calls, 3) + + def test_delete_user_success_missing_user_at_end(self): + self.test_auth.app = FakeApp(iter([ + # 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.assertEquals(resp.status_int, 204) + self.assertEquals(self.test_auth.app.calls, 3) + + def test_delete_user_success_missing_token(self): + self.test_auth.app = FakeApp(iter([ + # 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.assertEquals(resp.status_int, 204) + self.assertEquals(self.test_auth.app.calls, 3) + + def test_delete_user_success_no_token(self): + self.test_auth.app = FakeApp(iter([ + # 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.assertEquals(resp.status_int, 204) + self.assertEquals(self.test_auth.app.calls, 2) + + def test_validate_token_bad_prefix(self): + resp = Request.blank('/auth/v2/.token/BAD_token' + ).get_response(self.test_auth) + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 204) + self.assertEquals(resp.headers.get('x-auth-groups'), 'act:usr,act') + self.assert_(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.assertEquals(resp.status_int, 404) + self.assert_('x-auth-groups' not in resp.headers) + self.assert_('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.assertEquals(resp.status_int, 204) + self.assertEquals(self.test_auth.app.calls, 1) + self.assertEquals(resp.headers.get('x-auth-groups'), 'act:usr,act') + self.assert_(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.assertEquals(resp.status_int, 404) + self.assertEquals(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.assertEquals(resp.status_int, 204) + self.assertEquals(self.test_auth.app.calls, 1) + self.assertEquals(resp.headers.get('x-auth-groups'), + 'act:usr,act,AUTH_cfa') + self.assert_(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.assertEquals(conn.__class__, auth.HTTPConnection) + self.assertEquals(conn.host, '127.0.0.1') + self.assertEquals(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.assertEquals(conn.__class__, auth.HTTPSConnection) + self.assertEquals(conn.host, '1.2.3.4') + self.assertEquals(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.assertEquals(conn.__class__, auth.HTTPConnection) + self.assertEquals(conn.host, '5.6.7.8') + self.assertEquals(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.assertEquals(conn.__class__, auth.HTTPSConnection) + self.assertEquals(conn.host, '5.6.7.8') + self.assertEquals(conn.port, 443) + + def test_get_itoken_fail_no_memcache(self): + exc = None + try: + self.test_auth.get_itoken({}) + except Exception, err: + exc = err + self.assertEquals(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.assert_(itk.startswith('AUTH_itk'), itk) + expires, groups = fmc.get('AUTH_/auth/%s' % itk) + self.assert_(expires > time(), expires) + self.assertEquals(groups, '.auth,.reseller_admin,AUTH_.auth') + + def test_get_admin_detail_fail_no_colon(self): + self.test_auth.app = FakeApp(iter([])) + self.assertEquals(self.test_auth.get_admin_detail(Request.blank('/')), + None) + self.assertEquals(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.assertEquals(self.test_auth.get_admin_detail(Request.blank('/', + headers={'X-Auth-Admin-User': 'act:usr'})), None) + self.assertEquals(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, err: + exc = err + self.assertEquals(str(exc), 'Could not get admin user object: ' + '/v1/AUTH_.auth/act/usr 503 Service Unavailable') + self.assertEquals(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.assertEquals(self.test_auth.app.calls, 1) + self.assertEquals(detail, {'account': 'act', + 'auth': 'plaintext:key', + 'groups': [{'name': 'act:usr'}, {'name': 'act'}, + {'name': '.admin'}]}) + + def test_credentials_match_success(self): + self.assert_(self.test_auth.credentials_match( + {'auth': 'plaintext:key'}, 'key')) + + def test_credentials_match_fail_no_details(self): + self.assert_(not self.test_auth.credentials_match(None, 'notkey')) + + def test_credentials_match_fail_plaintext(self): + self.assert_(not self.test_auth.credentials_match( + {'auth': 'plaintext:key'}, 'notkey')) + + def test_is_super_admin_success(self): + self.assert_(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.assert_(not self.test_auth.is_super_admin(Request.blank('/', + headers={'X-Auth-Admin-User': '.super_admin', + 'X-Auth-Admin-Key': 'bad'}))) + self.assert_(not self.test_auth.is_super_admin(Request.blank('/', + headers={'X-Auth-Admin-User': '.super_admin'}))) + self.assert_(not self.test_auth.is_super_admin(Request.blank('/'))) + + def test_is_super_admin_fail_bad_user(self): + self.assert_(not self.test_auth.is_super_admin(Request.blank('/', + headers={'X-Auth-Admin-User': 'bad', + 'X-Auth-Admin-Key': 'supertest'}))) + self.assert_(not self.test_auth.is_super_admin(Request.blank('/', + headers={'X-Auth-Admin-Key': 'supertest'}))) + self.assert_(not self.test_auth.is_super_admin(Request.blank('/'))) + + def test_is_reseller_admin_success_is_super_admin(self): + self.assert_(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.assert_(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.assert_(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.assert_(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.assert_(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.assert_(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.assert_(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.assert_(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.assert_(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.assert_(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.assert_(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.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 200) + self.assert_(resp.headers.get('x-auth-token', + '').startswith('AUTH_tk'), resp.headers.get('x-auth-token')) + self.assertEquals(resp.headers.get('x-auth-token'), + resp.headers.get('x-storage-token')) + self.assertEquals(resp.headers.get('x-storage-url'), + 'http://127.0.0.1:8080/v1/AUTH_cfa') + self.assertEquals(json.loads(resp.body), + {"storage": {"default": "local", + "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}) + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 204) + self.assertEquals(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.assertEquals(resp.status_int, 204) + self.assertEquals(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.assertEquals(resp.status_int, 204) + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 401) + self.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(resp.status_int, 401) + self.assertNotEquals(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.assertEquals(resp.status_int, 400) + self.assertEquals(resp.body, 'Token exceeds maximum length.') + + def test_crazy_authorization(self): + req = self._make_request('/v1/AUTH_account', headers={ + 'authorization': 'somebody elses header value'}) + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, 401) + self.assertEquals(resp.environ['swift.authorize'], + self.test_auth.denied_response) + + +if __name__ == '__main__': + unittest.main() diff --git a/gluster/swift/common/middleware/gswauth/webadmin/index.html b/gluster/swift/common/middleware/gswauth/webadmin/index.html new file mode 100644 index 0000000..cbc7c8a --- /dev/null +++ b/gluster/swift/common/middleware/gswauth/webadmin/index.html @@ -0,0 +1,552 @@ + + + + + + +
+
+
Swauth
+
+
+
+
+ + diff --git a/tox.ini b/tox.ini index b42092b..9dc78b5 100644 --- a/tox.ini +++ b/tox.ini @@ -38,5 +38,5 @@ commands = {posargs} [flake8] ignore = H builtins = _ -exclude = swiftkerbauth.py,.venv,.tox,dist,doc,test,*egg +exclude = swiftkerbauth.py,.venv,.tox,dist,doc,test,*egg,gswauth show-source = True -- cgit