summaryrefslogtreecommitdiffstats
path: root/gluster
diff options
context:
space:
mode:
authorLuis Pabon <lpabon@redhat.com>2013-10-10 16:53:28 -0400
committerLuis Pabon <lpabon@redhat.com>2013-10-13 19:36:20 -0700
commitac1ac5bd2171e61ba4332dcae33d8a433cef7a4f (patch)
tree9d836201435c83cf59a659124f004a03f488c1f5 /gluster
parent46cd43fdf401b16f9e1b588e5fc8d2c5dd599b37 (diff)
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 <lpabon@redhat.com> Reviewed-on: http://review.gluster.org/6069
Diffstat (limited to 'gluster')
-rw-r--r--gluster/swift/common/middleware/gswauth/.gitignore3
-rwxr-xr-xgluster/swift/common/middleware/gswauth/.unittests4
-rw-r--r--gluster/swift/common/middleware/gswauth/AUTHORS39
-rw-r--r--gluster/swift/common/middleware/gswauth/CHANGELOG62
-rw-r--r--gluster/swift/common/middleware/gswauth/LICENSE202
-rw-r--r--gluster/swift/common/middleware/gswauth/MANIFEST.in4
-rw-r--r--gluster/swift/common/middleware/gswauth/README.md71
-rw-r--r--gluster/swift/common/middleware/gswauth/babel.cfg2
-rwxr-xr-xgluster/swift/common/middleware/gswauth/bin/swauth-add-account69
-rwxr-xr-xgluster/swift/common/middleware/gswauth/bin/swauth-add-user99
-rwxr-xr-xgluster/swift/common/middleware/gswauth/bin/swauth-cleanup-tokens165
-rwxr-xr-xgluster/swift/common/middleware/gswauth/bin/swauth-delete-account60
-rwxr-xr-xgluster/swift/common/middleware/gswauth/bin/swauth-delete-user60
-rwxr-xr-xgluster/swift/common/middleware/gswauth/bin/swauth-list86
-rwxr-xr-xgluster/swift/common/middleware/gswauth/bin/swauth-prep59
-rwxr-xr-xgluster/swift/common/middleware/gswauth/bin/swauth-set-account-service73
-rw-r--r--gluster/swift/common/middleware/gswauth/doc/source/_static/.empty0
-rw-r--r--gluster/swift/common/middleware/gswauth/doc/source/_templates/.empty0
-rw-r--r--gluster/swift/common/middleware/gswauth/doc/source/api.rst466
-rw-r--r--gluster/swift/common/middleware/gswauth/doc/source/authtypes.rst10
-rw-r--r--gluster/swift/common/middleware/gswauth/doc/source/conf.py233
-rw-r--r--gluster/swift/common/middleware/gswauth/doc/source/details.rst159
-rw-r--r--gluster/swift/common/middleware/gswauth/doc/source/index.rst142
-rw-r--r--gluster/swift/common/middleware/gswauth/doc/source/license.rst225
-rw-r--r--gluster/swift/common/middleware/gswauth/doc/source/middleware.rst9
-rw-r--r--gluster/swift/common/middleware/gswauth/doc/source/swauth.rst9
-rw-r--r--gluster/swift/common/middleware/gswauth/etc/proxy-server.conf-sample78
-rw-r--r--gluster/swift/common/middleware/gswauth/locale/swauth.pot30
-rw-r--r--gluster/swift/common/middleware/gswauth/setup.cfg23
-rw-r--r--gluster/swift/common/middleware/gswauth/setup.py89
-rw-r--r--gluster/swift/common/middleware/gswauth/swauth/__init__.py23
-rw-r--r--gluster/swift/common/middleware/gswauth/swauth/authtypes.py103
-rw-r--r--gluster/swift/common/middleware/gswauth/swauth/middleware.py1546
-rw-r--r--gluster/swift/common/middleware/gswauth/swauth/swift_version.py71
-rw-r--r--gluster/swift/common/middleware/gswauth/test_swauth/__init__.py10
-rw-r--r--gluster/swift/common/middleware/gswauth/test_swauth/unit/__init__.py0
-rw-r--r--gluster/swift/common/middleware/gswauth/test_swauth/unit/test_authtypes.py64
-rw-r--r--gluster/swift/common/middleware/gswauth/test_swauth/unit/test_middleware.py3642
-rw-r--r--gluster/swift/common/middleware/gswauth/webadmin/index.html552
39 files changed, 8542 insertions, 0 deletions
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: <http://gholt.github.com/swauth/>
+
+See also <https://github.com/openstack/keystone> for the standard OpenStack
+auth service.
+
+
+NOTE
+----
+
+**Be sure to review the Sphinx-built docs at:
+<http://gholt.github.com/swauth/>**
+
+
+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] <account>')
+ parser.add_option('-s', '--suffix', dest='suffix',
+ default='', help='The suffix to use with the reseller prefix as the '
+ 'storage account name (default: <randomly-generated-uuid4>) 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] <account> <user> <password>')
+ 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: <randomly-generated-uuid4>) 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] <account>')
+ 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] <account> <user>')
+ 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] <account> <service> <name> <value>
+
+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
--- /dev/null
+++ b/gluster/swift/common/middleware/gswauth/doc/source/_static/.empty
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
--- /dev/null
+++ b/gluster/swift/common/middleware/gswauth/doc/source/_templates/.empty
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/<api version>/ HTTP/1.1
+ X-Auth-Admin-User: .super_admin
+ X-Auth-Admin-Key: swauthkey
+
+Example Curl Request::
+
+ curl -D - https://<endpoint>/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/<version>/<account> 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/<api version>/<account> HTTP/1.1
+ X-Auth-Admin-User: .super_admin
+ X-Auth-Admin-Key: swauthkey
+
+Example Curl Request::
+
+ curl -D - https://<endpoint>/auth/v2/<account> \
+ -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://<storage endpoint>/v1/<account_id>" },
+ },
+ "account_id": "<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/<api version>/<new_account> HTTP/1.1
+ X-Auth-Admin-User: .super_admin
+ X-Auth-Admin-Key: swauthkey
+
+Example Curl Request::
+
+ curl -D - https://<endpoint>/auth/v2/<new_account> \
+ -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/<api version>/<account> HTTP/1.1
+ X-Auth-Admin-User: .super_admin
+ X-Auth-Admin-Key: swauthkey
+
+Example Curl Request::
+
+ curl -XDELETE -D - https://<endpoint>/auth/v2/<account> \
+ -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/<version>/<account>/<user> 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": "<act>:<usr>"},
+ # The first group is a unique user identifier
+ {"name": "<account>"},
+ # The second group is the auth account name
+ {"name": "<additional-group>"}
+ # There may be additional groups, .admin being a
+ # special group indicating an account admin and
+ # .reseller_admin indicating a reseller admin.
+ ],
+ "auth": "<auth-type>:<key>"
+ # 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/<api version>/<account>/<user> HTTP/1.1
+ X-Auth-Admin-User: .super_admin
+ X-Auth-Admin-Key: swauthkey
+
+Example Curl Request::
+
+ curl -D - https://<endpoint>/auth/v2/<account>/<user> \
+ -H "X-Auth-Admin-User: .super_admin" \
+ -H "X-Auth-Admin-Key: swauthkey"
+
+Example Response::
+
+ HTTP/1.1 200 Ok
+
+ { "groups": [ { "name": "<account>:<user>" },
+ { "name": "<user>" },
+ { "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/<api version>/<account>/<user> 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://<endpoint>/auth/v2/<account>/<user> \
+ -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/<api version>/<account>/<user> HTTP/1.1
+ X-Auth-Admin-User: .super_admin
+ X-Auth-Admin-Key: swauthkey
+
+Example Curl Request::
+
+ curl -XDELETE -D - https://<endpoint>/auth/v2/<account>/<user> \
+ -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 <account>, 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/<api version>/<account>/.services HTTP/1.0
+ X-Auth-Admin-User: .super_admin
+ X-Auth-Admin-Key: swauthkey
+
+ {"storage": { "local": "<new endpoint>" }}
+
+Example Curl Request::
+
+ curl -XPOST -D - https://<endpoint>/auth/v2/<account>/.services \
+ -H "X-Auth-Admin-User: .super_admin" \
+ -H "X-Auth-Admin-Key: swauthkey" --data-binary \
+ '{ "storage": { "local": "<new endpoint>" }}'
+
+Example Response::
+
+ HTTP/1.1 200 OK
+
+ {"storage": {"default": "local", "local": "<new endpoint>" }}
+
+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/<api version>/<account>/.groups
+ X-Auth-Admin-User: .super_admin
+ X-Auth-Admin-Key: swauthkey
+
+Example Curl Request::
+
+ curl -D - https://<endpoint>/auth/v2/<account>/.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": "<account>" },
+ { "name": "<account>:user1" },
+ { "name": "<account>: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
+# "<project> v<release> 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 <link> 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
+<auth_account>.
+
+The containers whose names do not begin with a period represent the accounts
+within the auth service. For example, the <auth_account>/test container would
+represent the "test" account.
+
+The objects within each container represent the users for that auth service
+account. For example, the <auth_account>/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": "<auth_type>:<auth_value>", "groups": <groups_array>}
+
+The `<auth_type>` specifies how the user key is encoded. The default is `plaintext`,
+which saves the user's key in plaintext in the `<auth_value>` 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 `<auth_type>` 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>
+ auth_type_salt = <salt-value>
+
+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 `<groups_array>` contains at least two groups. The first is a unique group
+identifying that user and it's name is of the format `<user>:<account>`. The
+second group is the `<account>` 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
+<auth_account>/<account> container. To map back the other way, an
+<auth_account>/.account_id/<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
+<auth_account>/<account>/.services object is created with its contents having a
+JSON dictionary of the format::
+
+ {"storage": {"default": "local", "local": <url>}}
+
+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
+`<auth_account>/.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": <account>,
+ "user": <user>,
+ "account_id": <account_id>,
+ "groups": <groups_array>,
+ "expires": <time.time() value>}
+
+The `<account>` is the auth service account's name for that token. The `<user>`
+is the user within the account for that token. The `<account_id>` is the
+same as the `X-Container-Meta-Account-Id` for the auth service's account,
+as described above. The `<groups_array>` 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 <auth_account>::
+
+ .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 <http://swift.openstack.org/development_auth.html>`_ and `acl callback <http://swift.openstack.org/misc.html#module-swift.common.middleware.acl>`_ 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 <api_top>`.
+
+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 <api_set_service_endpoints>`).
+
+
+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 = <your setting as discussed below>
+
+ 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://<public_ip>:<port>/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://<load_balancer_ip>:<port>/v1#http://127.0.0.1:<port>/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]://<host>:<port>/auth/ -K
+ swauthkey -a test tester testing``
+
+8) Ensure it works ``swift -A http[s]://<host>:<port>/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]://<host>:<port>/auth/v1.0
+ -U .super_admin:.super_admin -K swauthkey upload .webadmin .``
+
+3) Open ``http[s]://<host>:<port>/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 = <same as 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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\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:<key>"
+- 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/<account> 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/<account>/.services call for setting services
+ information. Can only be called by a reseller .admin.
+
+ In the :func:`handle_get_account` (GET v2/<account>) 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
+ <account>, 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/<account> 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/<account> 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/<account>/<user> 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": "<act>:<usr>"},
+ # The first group is a unique user identifier
+ {"name": "<account>"},
+ # The second group is the auth account name
+ {"name": "<additional-group>"}
+ # There may be additional groups, .admin being a special
+ # group indicating an account admin and .reseller_admin
+ # indicating a reseller admin.
+ ],
+ "auth": "plaintext:<key>"
+ # 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 <user> 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/<account>/<user> 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/<account>/<user> 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 <auth-prefix>/v1/<act>/auth
+ X-Auth-User: <act>:<usr> or X-Storage-User: <usr>
+ X-Auth-Key: <key> or X-Storage-Pass: <key>
+ GET <auth-prefix>/auth
+ X-Auth-User: <act>:<usr> or X-Storage-User: <act>:<usr>
+ X-Auth-Key: <key> or X-Storage-Pass: <key>
+ GET <auth-prefix>/v1.0
+ X-Auth-User: <act>:<usr> or X-Storage-User: <act>:<usr>
+ X-Auth-Key: <key> or X-Storage-Pass: <key>
+
+ 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": "<URL to use with Swift>",
+ # 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": "<URL to use with Swift>"
+ # 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/<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
--- /dev/null
+++ b/gluster/swift/common/middleware/gswauth/test_swauth/unit/__init__.py
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 @@
+<html>
+ <head>
+ <style type="text/css">
+ body {font-family: sans-serif}
+ table {border-collapse: collapse}
+ td {padding-left: 1ex; padding-right: 1ex}
+ .account {color: #0000ff; padding-left: 3ex; cursor: pointer}
+ .add_account_heading {text-align: right; padding-right: 0}
+ .service {padding-left: 3ex; vertical-align: top}
+ .service_detail {padding-left: 0}
+ .user {color: #0000ff; padding-left: 3ex; cursor: pointer}
+ .group {padding-left: 3ex}
+ .add_user_heading {text-align: right; padding-right: 0}
+ .shadow_delement {color: #0000ff; cursor: pointer}
+ .shadow_felement {display: none}
+ #swauth {font-size: 200%; font-weight: bold; font-style: italic; margin: 0px; padding: 0px}
+ #creds_area {float: right}
+ #logout {color: #0000ff; padding-left: 3ex; cursor: pointer}
+ #refresh_accounts {color: #0000ff; padding-left: 1ex; cursor: pointer}
+ #add_account {color: #0000ff; padding-left: 1ex; padding-right: 1ex; cursor: pointer}
+ #add_account_title {padding-top: 1ex; padding-bottom: 1ex}
+ #add_account_cancel {color: #0000ff; padding-top: 1ex; padding-left: 3ex; cursor: pointer}
+ #add_account_save {color: #0000ff; text-align: right; padding-top: 1ex; padding-right: 3ex; cursor: pointer}
+ #account_area {background: #ddeeff}
+ #add_user {color: #0000ff; padding-left: 1ex; padding-right: 1ex; cursor: pointer}
+ #add_user_title {padding-top: 1ex; padding-bottom: 1ex}
+ #add_user_cancel {color: #0000ff; padding-top: 1ex; padding-left: 3ex; cursor: pointer}
+ #add_user_save {color: #0000ff; text-align: right; padding-top: 1ex; padding-right: 3ex; cursor: pointer}
+ #delete_account {color: #0000ff; text-align: right; margin-left: 45ex; padding-right: 1ex; cursor: pointer}
+ #user_area {background: #aaccff}
+ #delete_user {color: #0000ff; text-align: right; margin-left: 45ex; padding-right: 1ex; cursor: pointer}
+ #auth_view {display: none}
+ #auth_toggler {color: #0000ff; cursor: pointer}
+ #auth_update {color: #0000ff; padding-left: 1ex; cursor: pointer}
+ #auth_update_field {display: none}
+ </style>
+ <script type="text/javascript">
+ var request = null;
+ var creds_user = '';
+ var creds_key = '';
+ var creds_logged_in = true;
+ var account = '';
+ var user = '';
+ var account_selection = -1;
+ var user_selection = -1;
+ var swauth_area_selected_background = '#ddeeff';
+ var account_area_selected_background = '#aaccff';
+ var endpoints;
+
+ function get_bounds(element) {
+ bounds = {};
+ bounds.top = 0;
+ bounds.left = 0;
+ bounds.width = element.offsetWidth;
+ bounds.height = element.offsetHeight;
+ if (element.offsetParent) {
+ do {
+ bounds.top += element.offsetTop;
+ bounds.left += element.offsetLeft;
+ } while (element = element.offsetParent);
+ }
+ return bounds;
+ }
+
+ function shadow_edit(delement) {
+ felement = document.getElementById('f' + delement.id.substring(1));
+ felement.value = delement.innerHTML;
+ delement.style.display = 'none';
+ felement.style.display = 'inline';
+ felement.focus();
+ }
+
+ function shadow_submitter(felement, evnt, func) {
+ keycode = 0;
+ if (window.event) {
+ keycode = window.event.keyCode;
+ } else if (evnt) {
+ keycode = evnt.which;
+ }
+ if (keycode == 13) {
+ func(felement);
+ return false;
+ }
+ return true;
+ }
+
+ function shadow_escaper(felement, evnt) {
+ keycode = 0;
+ if (window.event) {
+ keycode = window.event.keyCode;
+ } else if (evnt) {
+ keycode = evnt.which;
+ }
+ if (keycode == 27) {
+ felement.style.display = 'none';
+ document.getElementById('d' + felement.id.substring(1)).style.display = 'inline';
+ return false;
+ }
+ return true;
+ }
+
+ function creds_clicked() {
+ creds_area = document.getElementById('creds_area');
+ if (creds_logged_in) {
+ creds_user = '';
+ creds_key = '';
+ creds_area.innerHTML = 'User: <input id="creds_user" type="text" size="10" /> &nbsp; Key: <input id="creds_key" type="password" size="10" onkeypress="return creds_submitter(event)" />';
+ document.getElementById('swauth_area').innerHTML = '';
+ creds_logged_in = false;
+ document.getElementById("creds_user").focus();
+ } else {
+ creds_user = document.getElementById('creds_user').value;
+ creds_key = document.getElementById('creds_key').value;
+ creds_area.innerHTML = '<div>Logged in as ' + creds_user + ' <span id="logout" onclick="creds_clicked()">Logout</span></div>';
+ creds_logged_in = true;
+ swauth_area_load();
+ }
+ }
+
+ function creds_submitter(e) {
+ keycode = 0;
+ if (window.event) {
+ keycode = window.event.keyCode;
+ } else if (e) {
+ keycode = e.which;
+ }
+ if (keycode == 13) {
+ creds_clicked();
+ return false;
+ }
+ return true;
+ }
+
+ function swauth_area_reset() {
+ account_area_reset();
+ document.getElementById('swauth_area').innerHTML = '';
+ }
+
+ function account_area_reset() {
+ user_area_reset();
+ element = document.getElementById('add_account')
+ if (element) {
+ element.style.background = 'none';
+ }
+ if (account_selection != -1) {
+ document.getElementById('account_' + account_selection).style.background = 'none';
+ }
+ account = '';
+ account_selection = -1;
+ document.getElementById('account_area').innerHTML = '';
+ }
+
+ function user_area_reset() {
+ element = document.getElementById('add_user')
+ if (element) {
+ element.style.background = 'none';
+ }
+ if (user_selection != -1) {
+ document.getElementById('user_' + user_selection).style.background = 'none';
+ }
+ user = '';
+ user_selection = -1;
+ document.getElementById('user_area').innerHTML = '';
+ }
+
+ function swauth_area_load() {
+ swauth_area_reset();
+ request = new XMLHttpRequest();
+ request.onreadystatechange = swauth_area_load2;
+ request.open('GET', '/auth/v2/', true);
+ request.setRequestHeader('X-Auth-Admin-User', creds_user);
+ request.setRequestHeader('X-Auth-Admin-Key', creds_key);
+ request.send();
+ }
+
+ function swauth_area_load2() {
+ if (request.readyState == 4) {
+ swauth_area = document.getElementById('swauth_area');
+ if (request.status >= 200 && request.status <= 299) {
+ data = JSON.parse(request.responseText);
+ content = '<table><tr><td>Accounts <span id="refresh_accounts" onclick="swauth_area_load()">Refresh</span> <span id="add_account" onclick="add_account()">Add</span></td></tr>';
+ for (ix = 0; ix < data.accounts.length; ix++) {
+ content += '<tr><td id="account_' + ix + '" onclick="account_area_load(' + ix + ')" class="account">' + data.accounts[ix].name + '</td></tr>';
+ }
+ content += '</table>';
+ swauth_area.innerHTML = content;
+ } else {
+ swauth_area.innerHTML = 'Server returned status: ' + request.status + ' ' + request.statusText;
+ }
+ }
+ }
+
+ function add_account() {
+ account_area_reset();
+ document.getElementById('add_account').style.background = swauth_area_selected_background;
+ account_area = document.getElementById('account_area');
+ account_area.innerHTML = '<table><tr><td id="add_account_title" colspan="2">New Account</td></tr><tr><td class="add_account_heading">Name</td><td><input id="add_account_name" type="text" size="20" /></td></tr><tr><td class="add_account_heading">Suffix</td><td><input id="add_account_suffix" type="text" size="20" /> (Optional)</td></tr><tr><td id="add_account_cancel" onclick="swauth_area_load()">Cancel</td><td id="add_account_save" onclick="add_account_save()">Add</td></tr></table>';
+ bounds = get_bounds(document.getElementById('add_account'));
+ account_area.style.position = 'absolute';
+ account_area.style.top = bounds.top;
+ account_area.style.left = bounds.left + bounds.width;
+ document.getElementById("add_account_name").focus();
+ }
+
+ function add_account_save() {
+ request = new XMLHttpRequest();
+ request.onreadystatechange = add_account_save2;
+ request.open('PUT', '/auth/v2/' + document.getElementById('add_account_name').value, true);
+ request.setRequestHeader('X-Auth-Admin-User', creds_user);
+ request.setRequestHeader('X-Auth-Admin-Key', creds_key);
+ request.setRequestHeader('X-Account-Suffix', document.getElementById('add_account_suffix').value);
+ request.send();
+ }
+
+ function add_account_save2() {
+ if (request.readyState == 4) {
+ if (request.status >= 200 && request.status <= 299) {
+ swauth_area_load();
+ } else {
+ alert('Server returned status: ' + request.status + ' ' + request.statusText);
+ }
+ }
+ }
+
+ function account_area_load(account_index) {
+ account_area_reset();
+ account_element = document.getElementById('account_' + account_index);
+ account_element.style.background = swauth_area_selected_background;
+ account_selection = account_index;
+ account = account_element.innerHTML;
+ request = new XMLHttpRequest();
+ request.onreadystatechange = account_area_load2;
+ request.open('GET', '/auth/v2/' + account, true);
+ request.setRequestHeader('X-Auth-Admin-User', creds_user);
+ request.setRequestHeader('X-Auth-Admin-Key', creds_key);
+ request.send();
+ }
+
+ function account_area_load2() {
+ account_area = document.getElementById('account_area');
+ if (request.readyState == 4) {
+ if (request.status >= 200 && request.status <= 299) {
+ data = JSON.parse(request.responseText);
+ content = '<div id="delete_account" onclick="delete_account()">Delete</div><table><tr><td>Account Id</td><td>' + data.account_id + '</td></tr></table><table><tr><td>Services</td></tr>';
+ services = [];
+ for (service in data.services) {
+ services.push(service);
+ }
+ services.sort();
+ for (ix = 0; ix < services.length; ix++) {
+ content += '<tr><td class="service">' + services[ix] + '</td><td class="service_detail"><table>';
+ if (data.services[services[ix]]['default']) {
+ content += '<tr><td>default</td><td><span id="d-' + services[ix] + '" class="shadow_delement" onclick="shadow_edit(this)">' + data.services[services[ix]]['default'] + '</span><input id="f-' + services[ix] + '" class="shadow_felement" type="text" size="40" onkeypress="return shadow_submitter(this, event, endpoint_save)" onkeydown="return shadow_escaper(this, event)" /></td></tr>';
+ }
+ endpoints = [];
+ for (name in data.services[services[ix]]) {
+ if (name != 'default') {
+ endpoints.push(name);
+ }
+ }
+ endpoints.sort();
+ for (iy = 0; iy < endpoints.length; iy++) {
+ content += '<tr><td>' + endpoints[iy] + '</td><td><span id="d' + iy + '-' + services[ix] + '" class="shadow_delement" onclick="shadow_edit(this)">' + data.services[services[ix]][endpoints[iy]] + '</span><input id="f' + iy + '-' + services[ix] + '" class="shadow_felement" type="text" size="40" onkeypress="return shadow_submitter(this, event, endpoint_save)" onkeydown="return shadow_escaper(this, event)" /></td></tr>';
+ }
+ content += '</table></td></tr>';
+ }
+ content += '</table><table><tr><td>Users <span id="add_user" onclick="add_user()">Add</span></td></tr>';
+ for (ix = 0; ix < data.users.length; ix++) {
+ content += '<tr><td id="user_' + ix + '" onclick="user_area_load(' + ix + ')" class="user">' + data.users[ix].name + '</td></tr>';
+ }
+ content += '</table>';
+ account_area.innerHTML = content;
+ } else {
+ account_area.innerHTML = 'Server returned status: ' + request.status + ' ' + request.statusText;
+ }
+ bounds = get_bounds(document.getElementById('account_' + account_selection));
+ account_area.style.position = 'absolute';
+ account_area.style.top = bounds.top;
+ account_area.style.left = bounds.left + bounds.width;
+ }
+ }
+
+ function endpoint_save(field) {
+ service = field.id.substring(field.id.indexOf('-') + 1)
+ index = field.id.substring(1, field.id.indexOf('-'))
+ if (index) {
+ endpoint = endpoints[index];
+ } else {
+ endpoint = 'default';
+ }
+ services = {};
+ services[service] = {};
+ services[service][endpoint] = field.value;
+ request = new XMLHttpRequest();
+ request.onreadystatechange = endpoint_save2;
+ request.open('POST', '/auth/v2/' + account + '/.services', true);
+ request.setRequestHeader('X-Auth-Admin-User', creds_user);
+ request.setRequestHeader('X-Auth-Admin-Key', creds_key);
+ request.send(JSON.stringify(services));
+ }
+
+ function endpoint_save2() {
+ if (request.readyState == 4) {
+ if (request.status >= 200 && request.status <= 299) {
+ account_area_load(account_selection);
+ } else {
+ alert('Server returned status: ' + request.status + ' ' + request.statusText);
+ }
+ }
+ }
+
+ function add_user() {
+ user_area_reset();
+ document.getElementById('add_user').style.background = account_area_selected_background;
+ user_area = document.getElementById('user_area');
+ user_area.innerHTML = '<table><tr><td id="add_user_title" colspan="2">New User</td></tr><tr><td class="add_user_heading">Name</td><td><input id="add_user_name" type="text" size="20" /></td></tr><tr><td class="add_user_heading">Auth Key</td><td><input id="add_user_key" type="password" size="20" /></td></tr><tr><td class="add_user_heading">Account Admin</td><td><input id="add_user_admin" type="checkbox" /></td></tr><tr><td class="add_user_heading">Reseller Admin</td><td><input id="add_user_reseller_admin" type="checkbox" /></td></tr><tr><td id="add_user_cancel" onclick="add_user_cancel()">Cancel</td><td id="add_user_save" onclick="add_user_save()">Add</td></tr></table>';
+ bounds = get_bounds(document.getElementById('add_user'));
+ user_area.style.position = 'absolute';
+ user_area.style.top = bounds.top;
+ user_area.style.left = bounds.left + bounds.width;
+ document.getElementById("add_user_name").focus();
+ }
+
+ function add_user_cancel() {
+ document.getElementById('add_user').style.background = 'none';
+ document.getElementById('user_area').innerHTML = '';
+ }
+
+ function add_user_save() {
+ request = new XMLHttpRequest();
+ request.onreadystatechange = add_user_save2;
+ request.open('PUT', '/auth/v2/' + account + '/' + document.getElementById('add_user_name').value, true);
+ request.setRequestHeader('X-Auth-Admin-User', creds_user);
+ request.setRequestHeader('X-Auth-Admin-Key', creds_key);
+ request.setRequestHeader('X-Auth-User-Key', document.getElementById('add_user_key').value);
+ if (document.getElementById('add_user_admin').value) {
+ request.setRequestHeader('X-Auth-User-Admin', 'true');
+ }
+ if (document.getElementById('add_user_reseller_admin').value) {
+ request.setRequestHeader('X-Auth-User-Reseller-Admin', 'true');
+ }
+ request.send();
+ }
+
+ function add_user_save2() {
+ if (request.readyState == 4) {
+ if (request.status >= 200 && request.status <= 299) {
+ account_area_load(account_selection);
+ } else {
+ alert('Server returned status: ' + request.status + ' ' + request.statusText);
+ }
+ }
+ }
+
+ function delete_account() {
+ request = new XMLHttpRequest();
+ request.onreadystatechange = delete_account2;
+ request.open('DELETE', '/auth/v2/' + account, true);
+ request.setRequestHeader('X-Auth-Admin-User', creds_user);
+ request.setRequestHeader('X-Auth-Admin-Key', creds_key);
+ request.send();
+ }
+
+ function delete_account2() {
+ if (request.readyState == 4) {
+ if (request.status >= 200 && request.status <= 299) {
+ swauth_area_load();
+ } else {
+ alert('Server returned status: ' + request.status + ' ' + request.statusText);
+ }
+ }
+ }
+
+ function user_area_load(account_area_user_index) {
+ user_area_reset();
+ user_element = document.getElementById('user_' + account_area_user_index);
+ user_element.style.background = account_area_selected_background;
+ user_selection = account_area_user_index;
+ user = user_element.innerHTML;
+ request = new XMLHttpRequest();
+ request.onreadystatechange = user_area_load2;
+ request.open('GET', '/auth/v2/' + account + '/' + user, true);
+ request.setRequestHeader('X-Auth-Admin-User', creds_user);
+ request.setRequestHeader('X-Auth-Admin-Key', creds_key);
+ request.send();
+ }
+
+ function user_area_load2() {
+ user_area = document.getElementById('user_area');
+ if (request.readyState == 4) {
+ if (request.status >= 200 && request.status <= 299) {
+ data = JSON.parse(request.responseText);
+ content = '<div id="delete_user" onclick="delete_user()">Delete</div><table><tr><td>Auth</td><td><span id="auth_toggler" onclick="auth_toggle()">Show</span> <span id="auth_view">' + data.auth + '</span></td><td><input id="auth_update_field" type="password" size="20" onkeypress="return auth_submitter(event)" onkeydown="return auth_escaper(event)" /> <span id="auth_update" onclick="auth_update()">Update</span></td></tr></table><table><tr><td>Groups</td></tr>';
+ groups = [];
+ for (ix = 0; ix < data.groups.length; ix++) {
+ groups.push(data.groups[ix].name);
+ }
+ groups.sort();
+ for (ix = 0; ix < groups.length; ix++) {
+ content += '<tr><td class="group">' + groups[ix] + '</td></tr>';
+ }
+ content += '</table>';
+ user_area.innerHTML = content;
+ } else {
+ user_area.innerHTML = 'Server returned status: ' + request.status + ' ' + request.statusText;
+ }
+ bounds = get_bounds(document.getElementById('user_' + user_selection));
+ user_area.style.position = 'absolute';
+ user_area.style.top = bounds.top;
+ user_area.style.left = bounds.left + bounds.width;
+ }
+ }
+
+ function delete_user() {
+ request = new XMLHttpRequest();
+ request.onreadystatechange = delete_user2;
+ request.open('DELETE', '/auth/v2/' + account + '/' + user, true);
+ request.setRequestHeader('X-Auth-Admin-User', creds_user);
+ request.setRequestHeader('X-Auth-Admin-Key', creds_key);
+ request.send();
+ }
+
+ function delete_user2() {
+ if (request.readyState == 4) {
+ if (request.status >= 200 && request.status <= 299) {
+ account_area_load(account_selection);
+ } else {
+ alert('Server returned status: ' + request.status + ' ' + request.statusText);
+ }
+ }
+ }
+
+ function auth_toggle() {
+ to_toggle = document.getElementById('auth_view');
+ toggler = document.getElementById('auth_toggler');
+ if (to_toggle.style.display && to_toggle.style.display != 'none') {
+ toggler.innerHTML = 'Show';
+ to_toggle.style.display = 'none';
+ } else {
+ toggler.innerHTML = 'Hide';
+ to_toggle.style.display = 'inline';
+ }
+ }
+
+ function auth_update() {
+ field = document.getElementById('auth_update_field');
+ trigger = document.getElementById('auth_update');
+ if (field.style.display && field.style.display != 'none') {
+ auth_save();
+ } else {
+ field.style.display = 'inline';
+ trigger.style.display = 'none';
+ field.focus();
+ }
+ }
+
+ function auth_submitter(e) {
+ keycode = 0;
+ if (window.event) {
+ keycode = window.event.keyCode;
+ } else if (e) {
+ keycode = e.which;
+ }
+ if (keycode == 13) {
+ auth_save();
+ return false;
+ }
+ return true;
+ }
+
+ function auth_escaper(e) {
+ keycode = 0;
+ if (window.event) {
+ keycode = window.event.keyCode;
+ } else if (e) {
+ keycode = e.which;
+ }
+ if (keycode == 27) {
+ field = document.getElementById('auth_update_field');
+ field.value = '';
+ field.style.display ='none';
+ document.getElementById('auth_update').style.display ='inline';
+ return false;
+ }
+ return true;
+ }
+
+ function auth_save() {
+ document.getElementById('auth_update_field').style.display ='none';
+ if (document.getElementById('auth_update_field').value) {
+ request = new XMLHttpRequest();
+ request.onreadystatechange = auth_save2;
+ request.open('GET', '/auth/v2/' + account + '/' + user, true);
+ request.setRequestHeader('X-Auth-Admin-User', creds_user);
+ request.setRequestHeader('X-Auth-Admin-Key', creds_key);
+ request.send();
+ }
+ }
+
+ function auth_save2() {
+ if (request.readyState == 4) {
+ if (request.status >= 200 && request.status <= 299) {
+ data = JSON.parse(request.responseText);
+ request = new XMLHttpRequest();
+ request.onreadystatechange = auth_save3;
+ request.open('PUT', '/auth/v2/' + account_element.innerHTML + '/' + user_element.innerHTML, true);
+ request.setRequestHeader('X-Auth-Admin-User', creds_user);
+ request.setRequestHeader('X-Auth-Admin-Key', creds_key);
+ request.setRequestHeader('X-Auth-User-Key', document.getElementById('auth_update_field').value);
+ admin = false;
+ reseller_admin = false;
+ for (ix = 0; ix < data.groups.length; ix++) {
+ if (data.groups[ix].name == '.admin') {
+ admin = true;
+ } else if (data.groups[ix].name == '.reseller_admin') {
+ reseller_admin = true;
+ }
+ }
+ if (admin) {
+ request.setRequestHeader('X-Auth-User-Admin', 'true');
+ }
+ if (reseller_admin) {
+ request.setRequestHeader('X-Auth-User-Reseller-Admin', 'true');
+ }
+ request.send();
+ } else {
+ alert('Server returned status: ' + request.status + ' ' + request.statusText);
+ }
+ }
+ }
+
+ function auth_save3() {
+ if (request.readyState == 4) {
+ if (request.status >= 200 && request.status <= 299) {
+ user_area_load(user_selection);
+ } else {
+ alert('Server returned status: ' + request.status + ' ' + request.statusText);
+ }
+ }
+ }
+ </script>
+ </head>
+ <body onload="creds_clicked()">
+ <form onsubmit="return false">
+ <div id="creds_area"></div>
+ <div id="swauth">Swauth</div>
+ <div id="swauth_area"></div>
+ <div id="account_area"></div>
+ <div id="user_area"></div>
+ </form>
+ </body>
+</html>