pax_global_header00006660000000000000000000000064117350204710014512gustar00rootroot0000000000000052 comment=3782d56ce48867ef9130ced6873078073cf5162a gholt-swauth-3782d56/000077500000000000000000000000001173502047100144245ustar00rootroot00000000000000gholt-swauth-3782d56/.gitignore000066400000000000000000000000361173502047100164130ustar00rootroot00000000000000*.egg-info *.py[co] .DS_Store gholt-swauth-3782d56/.unittests000077500000000000000000000001631173502047100164720ustar00rootroot00000000000000#!/bin/bash nosetests test_swauth/unit --exe --with-coverage --cover-package swauth --cover-erase rm -f .coverage gholt-swauth-3782d56/AUTHORS000066400000000000000000000010241173502047100154710ustar00rootroot00000000000000Maintainer ---------- OpenStack, LLC. IRC: #openstack on irc.freenode.net Original Authors ---------------- Michael Barton John Dickinson Greg Holt Greg Lange Jay Payne Will Reese Chuck Thier Contributors ------------ Chmouel Boudjnah Anne Gentle Clay Gerrard David Goetz Soren Hansen Paul Jimenez Brian K. Jones Ed Leafe Pablo Llopis Stephen Milton Russ Nelson Colin Nicholson Andrew Clay Shafer Scott Simpson Monty Taylor Caleb Tennis FUJITA Tomonori Kapil Thangavelu Conrad Weidenkeller Chris Wedgwood Cory Wright Pete Zaitcev gholt-swauth-3782d56/CHANGELOG000066400000000000000000000015441173502047100156420ustar00rootroot00000000000000swauth (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. gholt-swauth-3782d56/LICENSE000066400000000000000000000264501173502047100154400ustar00rootroot00000000000000 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. gholt-swauth-3782d56/MANIFEST.in000066400000000000000000000001431173502047100161600ustar00rootroot00000000000000include AUTHORS LICENSE .unittests test_swauth/__init__.py include CHANGELOG graft doc graft etc gholt-swauth-3782d56/README.md000066400000000000000000000037021173502047100157050ustar00rootroot00000000000000Swauth ------ An Auth Service for Swift as WSGI Middleware that uses Swift itself as a backing store. Sphinx-built docs at: See also for the standard OpenStack auth service. NOTE ---- **Be sure to review the Sphinx-built docs at: ** Quick Install ------------- 1) Install Swauth with ``sudo python setup.py install`` or ``sudo python setup.py develop`` or via whatever packaging system you may be using. 2) Alter your proxy-server.conf pipeline to have swauth instead of tempauth: Was: [pipeline:main] pipeline = catch_errors cache tempauth proxy-server Change To: [pipeline:main] pipeline = catch_errors cache swauth proxy-server 3) Add to your proxy-server.conf the section for the Swauth WSGI filter: [filter:swauth] use = egg:swauth#swauth set log_name = swauth super_admin_key = swauthkey 4) Be sure your proxy server allows account management: [app:proxy-server] ... allow_account_management = true 5) Restart your proxy server ``swift-init proxy reload`` 6) Initialize the Swauth backing store in Swift ``swauth-prep -K swauthkey`` 7) Add an account/user ``swauth-add-user -A http://127.0.0.1:8080/auth/ -K swauthkey -a test tester testing`` 8) Ensure it works ``swift -A http://127.0.0.1:8080/auth/v1.0 -U test:tester -K testing stat -v`` Web Admin Install ----------------- 1) If you installed from packages, you'll need to cd to the webadmin directory the package installed. This is ``/usr/share/doc/python-swauth/webadmin`` with the Lucid packages. If you installed from source, you'll need to cd to the webadmin directory in the source directory. 2) Upload the Web Admin files with ``swift -A http://127.0.0.1:8080/auth/v1.0 -U .super_admin:.super_admin -K swauthkey upload .webadmin .`` 3) Open ``http://127.0.0.1:8080/auth/`` in your browser. gholt-swauth-3782d56/babel.cfg000066400000000000000000000000211173502047100161430ustar00rootroot00000000000000[python: **.py] gholt-swauth-3782d56/bin/000077500000000000000000000000001173502047100151745ustar00rootroot00000000000000gholt-swauth-3782d56/bin/swauth-add-account000077500000000000000000000055421173502047100206230ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (c) 2010-2011 OpenStack, LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import gettext from optparse import OptionParser from os.path import basename from sys import argv, exit from swift.common.bufferedhttp import http_connect_raw as http_connect from swift.common.utils import urlparse if __name__ == '__main__': gettext.install('swauth', unicode=1) parser = OptionParser(usage='Usage: %prog [options] ') parser.add_option('-s', '--suffix', dest='suffix', default='', help='The suffix to use with the reseller prefix as the ' 'storage account name (default: ) Note: If ' 'the account already exists, this will have no effect on existing ' 'service URLs. Those will need to be updated with ' 'swauth-set-account-service') parser.add_option('-A', '--admin-url', dest='admin_url', default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' 'subsystem (default: http://127.0.0.1:8080/auth/)') parser.add_option('-U', '--admin-user', dest='admin_user', default='.super_admin', help='The user with admin rights to add users ' '(default: .super_admin).') parser.add_option('-K', '--admin-key', dest='admin_key', help='The key for the user with admin rights to add users.') args = argv[1:] if not args: args.append('-h') (options, args) = parser.parse_args(args) if len(args) != 1: parser.parse_args(['-h']) account = args[0] parsed = urlparse(options.admin_url) if parsed.scheme not in ('http', 'https'): raise Exception('Cannot handle protocol scheme %s for url %s' % (parsed.scheme, repr(options.admin_url))) parsed_path = parsed.path if not parsed_path: parsed_path = '/' elif parsed_path[-1] != '/': parsed_path += '/' path = '%sv2/%s' % (parsed_path, account) headers = {'X-Auth-Admin-User': options.admin_user, 'X-Auth-Admin-Key': options.admin_key} 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)) gholt-swauth-3782d56/bin/swauth-add-user000077500000000000000000000102401173502047100201340ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (c) 2010-2011 OpenStack, LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import gettext from optparse import OptionParser from os.path import basename from sys import argv, exit from swift.common.bufferedhttp import http_connect_raw as http_connect from swift.common.utils import urlparse if __name__ == '__main__': gettext.install('swauth', unicode=1) parser = OptionParser( usage='Usage: %prog [options] ') parser.add_option('-a', '--admin', dest='admin', action='store_true', default=False, help='Give the user administrator access; otherwise ' 'the user will only have access to containers specifically allowed ' 'with ACLs.') parser.add_option('-r', '--reseller-admin', dest='reseller_admin', action='store_true', default=False, help='Give the user full reseller ' 'administrator access, giving them full access to all accounts within ' 'the reseller, including the ability to create new accounts. Creating ' 'a new reseller admin requires super_admin rights.') parser.add_option('-s', '--suffix', dest='suffix', default='', help='The suffix to use with the reseller prefix as the ' 'storage account name (default: ) Note: If ' 'the account already exists, this will have no effect on existing ' 'service URLs. Those will need to be updated with ' 'swauth-set-account-service') parser.add_option('-A', '--admin-url', dest='admin_url', default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' 'subsystem (default: http://127.0.0.1:8080/auth/') parser.add_option('-U', '--admin-user', dest='admin_user', default='.super_admin', help='The user with admin rights to add users ' '(default: .super_admin).') parser.add_option('-K', '--admin-key', dest='admin_key', help='The key for the user with admin rights to add users.') args = argv[1:] if not args: args.append('-h') (options, args) = parser.parse_args(args) if len(args) != 3: parser.parse_args(['-h']) account, user, password = args parsed = urlparse(options.admin_url) if parsed.scheme not in ('http', 'https'): raise Exception('Cannot handle protocol scheme %s for url %s' % (parsed.scheme, repr(options.admin_url))) parsed_path = parsed.path if not parsed_path: parsed_path = '/' elif parsed_path[-1] != '/': parsed_path += '/' # Ensure the account exists path = '%sv2/%s' % (parsed_path, account) headers = {'X-Auth-Admin-User': options.admin_user, 'X-Auth-Admin-Key': options.admin_key} if options.suffix: headers['X-Account-Suffix'] = options.suffix conn = http_connect(parsed.hostname, parsed.port, '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} 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)) gholt-swauth-3782d56/bin/swauth-cleanup-tokens000077500000000000000000000167111173502047100213710ustar00rootroot00000000000000#!/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 swift.common.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.' gholt-swauth-3782d56/bin/swauth-delete-account000077500000000000000000000046261173502047100213370ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (c) 2010-2011 OpenStack, LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import gettext from optparse import OptionParser from os.path import basename from sys import argv, exit from swift.common.bufferedhttp import http_connect_raw as http_connect from swift.common.utils import urlparse if __name__ == '__main__': gettext.install('swauth', unicode=1) parser = OptionParser(usage='Usage: %prog [options] ') parser.add_option('-A', '--admin-url', dest='admin_url', default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' 'subsystem (default: http://127.0.0.1:8080/auth/') parser.add_option('-U', '--admin-user', dest='admin_user', default='.super_admin', help='The user with admin rights to add users ' '(default: .super_admin).') parser.add_option('-K', '--admin-key', dest='admin_key', help='The key for the user with admin rights to add users.') args = argv[1:] if not args: args.append('-h') (options, args) = parser.parse_args(args) if len(args) != 1: parser.parse_args(['-h']) account = args[0] parsed = urlparse(options.admin_url) if parsed.scheme not in ('http', 'https'): raise Exception('Cannot handle protocol scheme %s for url %s' % (parsed.scheme, repr(options.admin_url))) parsed_path = parsed.path if not parsed_path: parsed_path = '/' elif parsed_path[-1] != '/': parsed_path += '/' path = '%sv2/%s' % (parsed_path, account) headers = {'X-Auth-Admin-User': options.admin_user, 'X-Auth-Admin-Key': options.admin_key} conn = http_connect(parsed.hostname, parsed.port, 'DELETE', path, headers, ssl=(parsed.scheme == 'https')) resp = conn.getresponse() if resp.status // 100 != 2: exit('Account deletion failed: %s %s' % (resp.status, resp.reason)) gholt-swauth-3782d56/bin/swauth-delete-user000077500000000000000000000046461173502047100206630ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (c) 2010-2011 OpenStack, LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import gettext from optparse import OptionParser from os.path import basename from sys import argv, exit from swift.common.bufferedhttp import http_connect_raw as http_connect from swift.common.utils import urlparse if __name__ == '__main__': gettext.install('swauth', unicode=1) parser = OptionParser(usage='Usage: %prog [options] ') parser.add_option('-A', '--admin-url', dest='admin_url', default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' 'subsystem (default: http://127.0.0.1:8080/auth/') parser.add_option('-U', '--admin-user', dest='admin_user', default='.super_admin', help='The user with admin rights to add users ' '(default: .super_admin).') parser.add_option('-K', '--admin-key', dest='admin_key', help='The key for the user with admin rights to add users.') args = argv[1:] if not args: args.append('-h') (options, args) = parser.parse_args(args) if len(args) != 2: parser.parse_args(['-h']) account, user = args parsed = urlparse(options.admin_url) if parsed.scheme not in ('http', 'https'): raise Exception('Cannot handle protocol scheme %s for url %s' % (parsed.scheme, repr(options.admin_url))) parsed_path = parsed.path if not parsed_path: parsed_path = '/' elif parsed_path[-1] != '/': parsed_path += '/' path = '%sv2/%s/%s' % (parsed_path, account, user) headers = {'X-Auth-Admin-User': options.admin_user, 'X-Auth-Admin-Key': options.admin_key} conn = http_connect(parsed.hostname, parsed.port, 'DELETE', path, headers, ssl=(parsed.scheme == 'https')) resp = conn.getresponse() if resp.status // 100 != 2: exit('User deletion failed: %s %s' % (resp.status, resp.reason)) gholt-swauth-3782d56/bin/swauth-list000077500000000000000000000065131173502047100174130ustar00rootroot00000000000000#!/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 gholt-swauth-3782d56/bin/swauth-prep000077500000000000000000000045451173502047100174110ustar00rootroot00000000000000#!/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)) gholt-swauth-3782d56/bin/swauth-set-account-service000077500000000000000000000055101173502047100223170ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (c) 2010-2011 OpenStack, LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. try: import simplejson as json except ImportError: import json import gettext from optparse import OptionParser from os.path import basename from sys import argv, exit from swift.common.bufferedhttp import http_connect_raw as http_connect from swift.common.utils import urlparse if __name__ == '__main__': gettext.install('swauth', unicode=1) parser = OptionParser(usage=''' Usage: %prog [options] Sets a service URL for an account. Can only be set by a reseller admin. Example: %prog -K swauthkey test storage local http://127.0.0.1:8080/v1/AUTH_018c3946-23f8-4efb-a8fb-b67aae8e4162 '''.strip()) parser.add_option('-A', '--admin-url', dest='admin_url', default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' 'subsystem (default: http://127.0.0.1:8080/auth/)') parser.add_option('-U', '--admin-user', dest='admin_user', default='.super_admin', help='The user with admin rights to add users ' '(default: .super_admin).') parser.add_option('-K', '--admin-key', dest='admin_key', help='The key for the user with admin rights to add users.') args = argv[1:] if not args: args.append('-h') (options, args) = parser.parse_args(args) if len(args) != 4: parser.parse_args(['-h']) account, service, name, url = args parsed = urlparse(options.admin_url) if parsed.scheme not in ('http', 'https'): raise Exception('Cannot handle protocol scheme %s for url %s' % (parsed.scheme, repr(options.admin_url))) parsed_path = parsed.path if not parsed_path: parsed_path = '/' elif parsed_path[-1] != '/': parsed_path += '/' path = '%sv2/%s/.services' % (parsed_path, account) body = json.dumps({service: {name: url}}) headers = {'Content-Length': str(len(body)), 'X-Auth-Admin-User': options.admin_user, 'X-Auth-Admin-Key': options.admin_key} conn = http_connect(parsed.hostname, parsed.port, 'POST', path, headers, ssl=(parsed.scheme == 'https')) conn.send(body) resp = conn.getresponse() if resp.status // 100 != 2: exit('Service set failed: %s %s' % (resp.status, resp.reason)) gholt-swauth-3782d56/doc/000077500000000000000000000000001173502047100151715ustar00rootroot00000000000000gholt-swauth-3782d56/doc/build/000077500000000000000000000000001173502047100162705ustar00rootroot00000000000000gholt-swauth-3782d56/doc/build/.gitignore000066400000000000000000000000021173502047100202500ustar00rootroot00000000000000* gholt-swauth-3782d56/doc/source/000077500000000000000000000000001173502047100164715ustar00rootroot00000000000000gholt-swauth-3782d56/doc/source/_static/000077500000000000000000000000001173502047100201175ustar00rootroot00000000000000gholt-swauth-3782d56/doc/source/_static/.empty000066400000000000000000000000001173502047100212440ustar00rootroot00000000000000gholt-swauth-3782d56/doc/source/_templates/000077500000000000000000000000001173502047100206265ustar00rootroot00000000000000gholt-swauth-3782d56/doc/source/_templates/.empty000066400000000000000000000000001173502047100217530ustar00rootroot00000000000000gholt-swauth-3782d56/doc/source/api.rst000066400000000000000000000311261173502047100177770ustar00rootroot00000000000000.. _api_top: ---------- Swauth API ---------- Overview ======== Swauth has its own internal versioned REST API for adding, removing, and editing accounts. This document explains the v2 API. Authentication -------------- Each REST request against the swauth API requires the inclusion of a specific authorization user and key to be passed in a specific HTTP header. These headers are defined as ``X-Auth-Admin-User`` and ``X-Auth-Admin-Key``. Typically, these values are ``.super_admin`` (the site super admin user) with the key being specified in the swauth middleware configuration as ``super_admin_key``. This could also be a reseller admin with the appropriate rights to perform actions on reseller accounts. Endpoints --------- The swauth API endpoint is presented on the proxy servers, in the "/auth" namespace. In addition, the API is versioned, and the version documented is version 2. API versions subdivide the auth namespace by version, specified as a version identifier like "v2". The auth endpoint described herein is therefore located at "/auth/v2/" as presented by the proxy servers. Bear in mind that in order for the auth management API to be presented, it must be enabled in the proxy server config by setting ``allow_account_managment`` to ``true`` in the ``[app:proxy-server]`` stanza of your proxy-server.conf. Responses --------- Responses from the auth APIs are returned as a JSON structure. Example return values in this document are edited for readability. Reseller/Admin Services ======================= Operations can be performed against the endpoint itself to perform general administrative operations. Currently, the only operations that can be performed is a GET operation to get reseller or site admin information. Get Admin Info -------------- A GET request at the swauth endpoint will return reseller information for the account specified in the ``X-Auth-Admin-User`` header. Currently, the information returned is limited to a list of accounts for the reseller or site admin. Valid return codes: * 200: Success * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key * 5xx: Internal error Example Request:: GET /auth// HTTP/1.1 X-Auth-Admin-User: .super_admin X-Auth-Admin-Key: swauthkey Example Curl Request:: curl -D - https:///auth/v2/ \ -H "X-Auth-Admin-User: .super_admin" \ -H "X-Auth-Admin-Key: swauthkey" Example Result:: HTTP/1.1 200 OK { "accounts": [ { "name": "account1" }, { "name": "account2" }, { "name": "account3" } ] } Account Services ================ There are API request to get account details, create, and delete accounts, mapping logically to the REST verbs GET, PUT, and DELETE. These actions are performed against an account URI, in the following general request structure:: METHOD /auth// HTTP/1.1 The methods that can be used are detailed below. Get Account Details ------------------- Account details can be retrieved by performing a GET request against an account URI. On success, a JSON dictionary will be returned containing the keys `account_id`, `services`, and `users`. The `account_id` is the value used when creating service accounts. The `services` value is a dict that represents valid storage cluster endpoints, and which endpoint is the default. The 'users' value is a list of dicts, each dict representing a user and currently only containing the single key 'name'. Valid Responses: * 200: Success * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key * 5xx: Internal error Example Request:: GET /auth// HTTP/1.1 X-Auth-Admin-User: .super_admin X-Auth-Admin-Key: swauthkey Example Curl Request:: curl -D - https:///auth/v2/ \ -H "X-Auth-Admin-User: .super_admin" \ -H "X-Auth-Admin-Key: swauthkey" Example Response:: HTTP/1.1 200 OK { "services": { "storage": { "default": "local", "local": "https:///v1/" }, }, "account_id": "", "users": [ { "name": "user1" }, { "name": "user2" } ] } Create Account -------------- An account can be created with a PUT request against a non-existent account. By default, a newly created UUID4 will be used with the reseller prefix as the account ID used when creating corresponding service accounts. However, you can provide an X-Account-Suffix header to replace the UUDI4 part. Valid return codes: * 200: Success * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key * 5xx: Internal error Example Request:: GET /auth// HTTP/1.1 X-Auth-Admin-User: .super_admin X-Auth-Admin-Key: swauthkey Example Curl Request:: curl -D - https:///auth/v2/ \ -H "X-Auth-Admin-User: .super_admin" \ -H "X-Auth-Admin-Key: swauthkey" Example Response:: HTTP/1.1 201 Created Delete Account -------------- An account can be deleted with a DELETE request against an existing account. Valid Responses: * 204: Success * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key * 404: Account not found * 5xx: Internal error Example Request:: DELETE /auth// HTTP/1.1 X-Auth-Admin-User: .super_admin X-Auth-Admin-Key: swauthkey Example Curl Request:: curl -XDELETE -D - https:///auth/v2/ \ -H "X-Auth-Admin-User: .super_admin" \ -H "X-Auth-Admin-Key: swauthkey" Example Response:: HTTP/1.1 204 No Content User Services ============= Each account in swauth contains zero or more users. These users can be determined with the 'Get Account Details' API request against an account. Users in an account can be created, modified, and detailed as described below by apply the appropriate REST verbs to a user URI, in the following general request structure:: METHOD /auth/// HTTP/1.1 The methods that can be used are detailed below. Get User Details ---------------- User details can be retrieved by performing a GET request against a user URI. On success, a JSON dictionary will be returned as described:: {"groups": [ # List of groups the user is a member of {"name": ":"}, # The first group is a unique user identifier {"name": ""}, # The second group is the auth account name {"name": ""} # There may be additional groups, .admin being a # special group indicating an account admin and # .reseller_admin indicating a reseller admin. ], "auth": ":" # The auth-type and key for the user; currently only # plaintext and sha1 are implemented as auth types. } For example:: {"groups": [{"name": "test:tester"}, {"name": "test"}, {"name": ".admin"}], "auth": "plaintext:testing"} Valid Responses: * 200: Success * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key * 404: Unknown account * 5xx: Internal error Example Request:: GET /auth/// HTTP/1.1 X-Auth-Admin-User: .super_admin X-Auth-Admin-Key: swauthkey Example Curl Request:: curl -D - https:///auth/v2// \ -H "X-Auth-Admin-User: .super_admin" \ -H "X-Auth-Admin-Key: swauthkey" Example Response:: HTTP/1.1 200 Ok { "groups": [ { "name": ":" }, { "name": "" }, { "name": ".admin" } ], "auth" : "plaintext:password" } Create User ----------- A user can be created with a PUT request against a non-existent user URI. The new user's password must be set using the ``X-Auth-User-Key`` header. The user name MUST NOT start with a period ('.'). This requirement is enforced by the API, and will result in a 400 error. Optional Headers: * ``X-Auth-User-Admin: true``: create the user as an account admin * ``X-Auth-User-Reseller-Admin: true``: create the user as a reseller admin Reseller admin accounts can only be created by the site admin, while regular accounts (or account admin accounts) can be created by an account admin, an appropriate reseller admin, or the site admin. Note that PUT requests are idempotent, and the PUT request serves as both a request and modify action. Valid Responses: * 200: Success * 400: Invalid request (missing required headers) * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key, or insufficient priv * 404: Unknown account * 5xx: Internal error Example Request:: PUT /auth/// HTTP/1.1 X-Auth-Admin-User: .super_admin X-Auth-Admin-Key: swauthkey X-Auth-User-Admin: true X-Auth-User-Key: secret Example Curl Request:: curl -XPUT -D - https:///auth/v2// \ -H "X-Auth-Admin-User: .super_admin" \ -H "X-Auth-Admin-Key: swauthkey" \ -H "X-Auth-User-Admin: true" \ -H "X-Auth-User-Key: secret" Example Response:: HTTP/1.1 201 Created Delete User ----------- A user can be deleted by performing a DELETE request against a user URI. This action can only be performed by an account admin, appropriate reseller admin, or site admin. Valid Responses: * 200: Success * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key, or insufficient priv * 404: Unknown account or user * 5xx: Internal error Example Request:: DELETE /auth/// HTTP/1.1 X-Auth-Admin-User: .super_admin X-Auth-Admin-Key: swauthkey Example Curl Request:: curl -XDELETE -D - https:///auth/v2// \ -H "X-Auth-Admin-User: .super_admin" \ -H "X-Auth-Admin-Key: swauthkey" Example Response:: HTTP/1.1 204 No Content Other Services ============== There are several other swauth functions that can be performed, mostly done via "pseudo-user" accounts. These are well-known user names that are unable to be actually provisioned. These pseudo-users are described below. .. _api_set_service_endpoints: Set Service Endpoints --------------------- Service endpoint information can be retrived using the _`Get Account Details` API method. This function allows setting values within this section for the , allowing the addition of new service end points or updating existing ones by performing a POST to the URI corresponding to the pseudo-user ".services". The body of the POST request should contain a JSON dict with the following format:: {"service_name": {"end_point_name": "end_point_value"}} There can be multiple services and multiple end points in the same call. Any new services or end points will be added to the existing set of services and end points. Any existing services with the same service name will be merged with the new end points. Any existing end points with the same end point name will have their values updated. The updated services dictionary will be returned on success. Valid Responses: * 200: Success * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key * 404: Account not found * 5xx: Internal error Example Request:: POST /auth///.services HTTP/1.0 X-Auth-Admin-User: .super_admin X-Auth-Admin-Key: swauthkey {"storage": { "local": "" }} Example Curl Request:: curl -XPOST -D - https:///auth/v2//.services \ -H "X-Auth-Admin-User: .super_admin" \ -H "X-Auth-Admin-Key: swauthkey" --data-binary \ '{ "storage": { "local": "" }}' Example Response:: HTTP/1.1 200 OK {"storage": {"default": "local", "local": "" }} Get Account Groups ------------------ Individual user group information can be retrieved using the `Get User Details`_ API method. This function allows retrieving all group information for all users in an existing account. This can be achieved using a GET action against a user URI with the pseudo-user ".groups". The JSON dictionary returned will be a "groups" dictionary similar to that documented in the `Get User Details`_ method, but representing the summary of all groups utilized by all active users in the account. Valid Responses: * 200: Success * 403: Invalid X-Auth-Admin-User/X-Auth-Admin-Key * 404: Account not found * 5xx: Internal error Example Request:: GET /auth///.groups X-Auth-Admin-User: .super_admin X-Auth-Admin-Key: swauthkey Example Curl Request:: curl -D - https:///auth/v2//.groups \ -H "X-Auth-Admin-User: .super_admin" \ -H "X-Auth-Admin-Key: swauthkey" Example Response:: HTTP/1.1 200 OK { "groups": [ { "name": ".admin" }, { "name": "" }, { "name": ":user1" }, { "name": ":user2" } ] } gholt-swauth-3782d56/doc/source/authtypes.rst000066400000000000000000000002521173502047100212500ustar00rootroot00000000000000.. _swauth_authtypes_module: swauth.authtypes ================= .. automodule:: swauth.authtypes :members: :undoc-members: :show-inheritance: :noindex: gholt-swauth-3782d56/doc/source/conf.py000066400000000000000000000167661173502047100200100ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2010-2011 OpenStack, LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # # Swauth documentation build configuration file, created by # sphinx-quickstart on Mon Feb 14 19:34:51 2011. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os import swauth # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Swauth' copyright = u'2010-2011, OpenStack, LLC' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '.'.join(str(v) for v in swauth.version_info[:2]) # The full version, including alpha/beta/rc tags. release = swauth.version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Swauthdoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Swauth.tex', u'Swauth Documentation', u'OpenStack, LLC', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'swauth', u'Swauth Documentation', [u'OpenStack, LLC'], 1) ] gholt-swauth-3782d56/doc/source/details.rst000066400000000000000000000151641173502047100206570ustar00rootroot00000000000000---------------------- Implementation Details ---------------------- The Swauth system is a scalable authentication and authorization system that uses Swift itself as its backing store. This section will describe how it stores its data. .. note:: You can access Swauth's internal .auth account by using the account:user of .super_admin:.super_admin and the super admin key you have set in your configuration. Here's an example using `st` on a standard SAIO: ``st -A http://127.0.0.1:8080/auth/v1.0 -U .super_admin:.super_admin -K swauthkey stat`` At the topmost level, the auth system has its own Swift account it stores its own account information within. This Swift account is known as self.auth_account in the code and its name is in the format self.reseller_prefix + ".auth". In this text, we'll refer to this account as . The containers whose names do not begin with a period represent the accounts within the auth service. For example, the /test container would represent the "test" account. The objects within each container represent the users for that auth service account. For example, the /test/bob object would represent the user "bob" within the auth service account of "test". Each of these user objects contain a JSON dictionary of the format:: {"auth": ":", "groups": } The `` specifies how the user key is encoded. The default is `plaintext`, which saves the user's key in plaintext in the `` field. The value `sha1` is supported as well, which stores the user's key as a salted SHA1 hash. Note that using a one-way hash like SHA1 will likely inhibit future use of key-signing request types, assuming such support is added. The `` can be specified in the swauth section of the proxy server's config file, along with the salt value in the following way:: auth_type = auth_type_salt = Both fields are optional. auth_type defaults to `plaintext` and auth_type_salt defaults to "swauthsalt". Additional auth types can be implemented along with existing ones in the authtypes.py module. The `` contains at least two groups. The first is a unique group identifying that user and it's name is of the format `:`. The second group is the `` itself. Additional groups of `.admin` for account administrators and `.reseller_admin` for reseller administrators may exist. Here's an example user JSON dictionary:: {"auth": "plaintext:testing", "groups": ["name": "test:tester", "name": "test", "name": ".admin"]} To map an auth service account to a Swift storage account, the Service Account Id string is stored in the `X-Container-Meta-Account-Id` header for the / container. To map back the other way, an /.account_id/ object is created with the contents of the corresponding auth service's account name. Also, to support a future where the auth service will support multiple Swift clusters or even multiple services for the same auth service account, an //.services object is created with its contents having a JSON dictionary of the format:: {"storage": {"default": "local", "local": }} The "default" is always "local" right now, and "local" is always the single Swift cluster URL; but in the future there can be more than one cluster with various names instead of just "local", and the "default" key's value will contain the primary cluster to use for that account. Also, there may be more services in addition to the current "storage" service right now. Here's an example .services dictionary at the moment:: {"storage": {"default": "local", "local": "http://127.0.0.1:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9"}} But, here's an example of what the dictionary may look like in the future:: {"storage": {"default": "dfw", "dfw": "http://dfw.storage.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9", "ord": "http://ord.storage.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9", "sat": "http://ord.storage.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9"}, "servers": {"default": "dfw", "dfw": "http://dfw.servers.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9", "ord": "http://ord.servers.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9", "sat": "http://ord.servers.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9"}} Lastly, the tokens themselves are stored as objects in the `/.token_[0-f]` containers. The names of the objects are the token strings themselves, such as `AUTH_tked86bbd01864458aa2bd746879438d5a`. The exact `.token_[0-f]` container chosen is based on the final digit of the token name, such as `.token_a` for the token `AUTH_tked86bbd01864458aa2bd746879438d5a`. The contents of the token objects are JSON dictionaries of the format:: {"account": , "user": , "account_id": , "groups": , "expires": } The `` is the auth service account's name for that token. The `` is the user within the account for that token. The `` is the same as the `X-Container-Meta-Account-Id` for the auth service's account, as described above. The `` is the user's groups, as described above with the user object. The "expires" value indicates when the token is no longer valid, as compared to Python's time.time() value. Here's an example token object's JSON dictionary:: {"account": "test", "user": "tester", "account_id": "AUTH_8980f74b1cda41e483cbe0a925f448a9", "groups": ["name": "test:tester", "name": "test", "name": ".admin"], "expires": 1291273147.1624689} To easily map a user to an already issued token, the token name is stored in the user object's `X-Object-Meta-Auth-Token` header. Here is an example full listing of an :: .account_id AUTH_2282f516-559f-4966-b239-b5c88829e927 AUTH_f6f57a3c-33b5-4e85-95a5-a801e67505c8 AUTH_fea96a36-c177-4ca4-8c7e-b8c715d9d37b .token_0 .token_1 .token_2 .token_3 .token_4 .token_5 .token_6 AUTH_tk9d2941b13d524b268367116ef956dee6 .token_7 .token_8 AUTH_tk93627c6324c64f78be746f1e6a4e3f98 .token_9 .token_a .token_b .token_c .token_d .token_e AUTH_tk0d37d286af2c43ffad06e99112b3ec4e .token_f AUTH_tk766bbde93771489982d8dc76979d11cf reseller .services reseller test .services tester tester3 test2 .services tester2 gholt-swauth-3782d56/doc/source/index.rst000066400000000000000000000157351173502047100203450ustar00rootroot00000000000000.. Swauth documentation master file, created by sphinx-quickstart on Mon Feb 14 19:34:51 2011. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Swauth ====== Copyright (c) 2010-2012 OpenStack, LLC An Auth Service for Swift as WSGI Middleware that uses Swift itself as a backing store. Sphinx-built docs at: http://gholt.github.com/swauth/ Source available at: https://github.com/gholt/swauth See also https://github.com/openstack/keystone for the standard OpenStack auth service. Overview -------- Before discussing how to install Swauth within a Swift system, it might help to understand how Swauth does it work first. 1. Swauth is middleware installed in the Swift Proxy's WSGI pipeline. 2. It intercepts requests to ``/auth/`` (by default). 3. It also uses Swift's `authorize callback `_ and `acl callback `_ features to authorize Swift requests. 4. Swauth will also make various internal calls to the Swift WSGI pipeline it's installed in to manipulate containers and objects within an ``AUTH_.auth`` (by default) Swift account. These containers and objects are what store account and user information. 5. Instead of #4, Swauth can be configured to call out to another remote Swauth to perform #4 on its behalf (using the swauth_remote config value). 6. When managing accounts and users with the various ``swauth-`` command line tools, these tools are actually just performing HTTP requests against the ``/auth/`` end point referenced in #2. You can make your own tools that use the same :ref:`API `. 7. In the special case of creating a new account, Swauth will do its usual WSGI-internal requests as per #4 but will also call out to the Swift cluster to create the actual Swift account. a. This Swift cluster callout is an account PUT request to the URL defined by the ``swift_default_cluster`` config value. b. This callout end point is also saved when the account is created so that it can be given to the users of that account in the future. c. Sometimes, due to public/private network routing or firewalling, the URL Swauth should use should be different than the URL Swauth should give the users later. That is why the ``default_swift_cluster`` config value can accept two URLs (first is the one for users, second is the one for Swauth). d. Once an account is created, the URL given to users for that account will not change, even if the ``default_swift_cluster`` config value changes. This is so that you can use multiple clusters with the same Swauth system; ``default_swift_cluster`` just points to the one where you want new users to go. f. You can change the stored URL for an account if need be with the ``swauth-set-account-service`` command line tool or a POST request (see :ref:`API `). Install ------- 1) Install Swauth with ``sudo python setup.py install`` or ``sudo python setup.py develop`` or via whatever packaging system you may be using. 2) Alter your ``proxy-server.conf`` pipeline to have ``swauth`` instead of ``tempauth``: Was:: [pipeline:main] pipeline = catch_errors cache tempauth proxy-server Change To:: [pipeline:main] pipeline = catch_errors cache swauth proxy-server 3) Add to your ``proxy-server.conf`` the section for the Swauth WSGI filter:: [filter:swauth] use = egg:swauth#swauth set log_name = swauth super_admin_key = swauthkey default_swift_cluster = The ``default_swift_cluster`` setting can be confusing. a. If you're using an all-in-one type configuration where everything will be run on the local host on port 8080, you can omit the ``default_swift_cluster`` completely and it will default to ``local#http://127.0.0.1:8080/v1``. b. If you're using a single Swift proxy you can just set the ``default_swift_cluster = cluster_name#https://:/v1`` and that URL will be given to users as well as used by Swauth internally. (Quick note: be sure the ``http`` vs. ``https`` is set right depending on if you're using SSL.) c. If you're using multiple Swift proxies behind a load balancer, you'll probably want ``default_swift_cluster = cluster_name#https://:/v1#http://127.0.0.1:/v1`` so that Swauth gives out the first URL but uses the second URL internally. Remember to double-check the ``http`` vs. ``https`` settings for each of the URLs; they might be different if you're terminating SSL at the load balancer. Also see the ``proxy-server.conf-sample`` for more config options, such as the ability to have a remote Swauth in a multiple Swift cluster configuration. 4) Be sure your Swift proxy allows account management in the ``proxy-server.conf``:: [app:proxy-server] ... allow_account_management = true For greater security, you can leave this off any public proxies and just have one or two private proxies with it turned on. 5) Restart your proxy server ``swift-init proxy reload`` 6) Initialize the Swauth backing store in Swift ``swauth-prep -K swauthkey`` 7) Add an account/user ``swauth-add-user -A http[s]://:/auth/ -K swauthkey -a test tester testing`` 8) Ensure it works ``swift -A http[s]://:/auth/v1.0 -U test:tester -K testing stat -v`` If anything goes wrong, it's best to start checking the proxy server logs. The client command line utilities often don't get enough information to help. I will often just ``tail -F`` the appropriate proxy log (``/var/log/syslog`` or however you have it configured) and then run the Swauth command to see exactly what requests are happening to try to determine where things fail. General note, I find I occasionally just forget to reload the proxies after a config change; so that's the first thing you might try. Or, if you suspect the proxies aren't reloading properly, you might try ``swift-init proxy stop``, ensure all the processes died, then ``swift-init proxy start``. Also, it's quite common to get the ``/auth/v1.0`` vs. just ``/auth/`` URL paths confused. Usual rule is: Swauth tools use just ``/auth/`` and Swift tools use ``/auth/v1.0``. Web Admin Install ----------------- 1) If you installed from packages, you'll need to cd to the webadmin directory the package installed. This is ``/usr/share/doc/python-swauth/webadmin`` with the Lucid packages. If you installed from source, you'll need to cd to the webadmin directory in the source directory. 2) Upload the Web Admin files with ``swift -A http[s]://:/auth/v1.0 -U .super_admin:.super_admin -K swauthkey upload .webadmin .`` 3) Open ``http[s]://:/auth/`` in your browser. Contents -------- .. toctree:: :maxdepth: 2 license details swauth middleware api authtypes Indices and tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` gholt-swauth-3782d56/doc/source/license.rst000066400000000000000000000273511173502047100206550ustar00rootroot00000000000000.. _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. gholt-swauth-3782d56/doc/source/middleware.rst000066400000000000000000000002371173502047100213420ustar00rootroot00000000000000.. _swauth_middleware_module: swauth.middleware ================= .. automodule:: swauth.middleware :members: :undoc-members: :show-inheritance: gholt-swauth-3782d56/doc/source/swauth.rst000066400000000000000000000001631173502047100205360ustar00rootroot00000000000000.. _swauth_module: swauth ====== .. automodule:: swauth :members: :undoc-members: :show-inheritance: gholt-swauth-3782d56/etc/000077500000000000000000000000001173502047100151775ustar00rootroot00000000000000gholt-swauth-3782d56/etc/proxy-server.conf-sample000066400000000000000000000062771173502047100220260ustar00rootroot00000000000000[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. # token_life = 86400 # 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 gholt-swauth-3782d56/locale/000077500000000000000000000000001173502047100156635ustar00rootroot00000000000000gholt-swauth-3782d56/locale/swauth.pot000066400000000000000000000015251173502047100177250ustar00rootroot00000000000000# Translations template for swauth. # Copyright (C) 2011 ORGANIZATION # This file is distributed under the same license as the swauth project. # FIRST AUTHOR , 2011. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: swauth 1.0.1.dev\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2011-05-26 10:35+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9.4\n" #: swauth/middleware.py:94 msgid "No super_admin_key set in conf file! Exiting." msgstr "" #: swauth/middleware.py:637 #, python-format msgid "" "ERROR: Exception while trying to communicate with " "%(scheme)s://%(host)s:%(port)s/%(path)s" msgstr "" gholt-swauth-3782d56/setup.cfg000066400000000000000000000005731173502047100162520ustar00rootroot00000000000000[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 gholt-swauth-3782d56/setup.py000066400000000000000000000052671173502047100161500ustar00rootroot00000000000000#!/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='openstack-admins@lists.launchpad.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', ], }, ) gholt-swauth-3782d56/swauth/000077500000000000000000000000001173502047100157375ustar00rootroot00000000000000gholt-swauth-3782d56/swauth/__init__.py000066400000000000000000000014571173502047100200570ustar00rootroot00000000000000# 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 #: Version information (major, minor, revision[, 'dev']). version_info = (1, 0, 4) #: Version string 'major.minor.revision'. version = __version__ = ".".join(map(str, version_info)) gettext.install('swauth') gholt-swauth-3782d56/swauth/authtypes.py000066400000000000000000000071621173502047100203450ustar00rootroot00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # # Pablo Llopis 2011 """ This module hosts available auth types for encoding and matching user keys. For adding a new auth type, simply write a class that satisfies the following conditions: - For the class name, capitalize first letter only. This makes sure the user can specify an all-lowercase config option such as "plaintext" or "sha1". Swauth takes care of capitalizing the first letter before instantiating it. - Write an encode(key) method that will take a single argument, the user's key, and returns the encoded string. For plaintext, this would be "plaintext:" - Write a match(key, creds) method that will take two arguments: the user's key, and the user's retrieved credentials. Return a boolean value that indicates whether the match is True or False. Note that, since some of the encodings will be hashes, swauth supports the notion of salts. Thus, self.salt will be set to either a user-specified salt value or to a default value. """ import hashlib #: Maximum length any valid token should ever be. MAX_TOKEN_LENGTH = 256 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 gholt-swauth-3782d56/swauth/middleware.py000066400000000000000000002072611173502047100204360ustar00rootroot00000000000000# 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 webob import Response, Request from webob.exc import HTTPAccepted, HTTPBadRequest, HTTPConflict, \ HTTPCreated, HTTPForbidden, HTTPMethodNotAllowed, HTTPMovedPermanently, \ HTTPNoContent, HTTPNotFound, HTTPServiceUnavailable, HTTPUnauthorized 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 from swift.common.wsgi import make_pre_authed_request import swauth.authtypes 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.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' def __call__(self, env, start_response): """ Accepts a standard WSGI application call, authenticating the request and installing callback hooks for authorization and ACL header validation. For an authenticated request, REMOTE_USER will be set to a comma separated list of the user's groups. With a non-empty reseller prefix, acts as the definitive auth service for just tokens and accounts that begin with that prefix, but will deny requests outside this prefix if no other auth middleware overrides it. With an empty reseller prefix, acts as the definitive auth service only for tokens that validate to a non-empty set of groups. For all other requests, acts as the fallback auth service when no other auth middleware overrides it. Alternatively, if the request matches the self.auth_prefix, the request will be routed through the internal auth request handler (self.handle). This is to handle creating users, accounts, granting tokens, etc. """ if 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 else: # Unauthorized token if 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 for empty # reseller_prefixed tokens, I won't overwrite swift.authorize. 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 account = env['HTTP_AUTHORIZATION'].split(' ')[1] account, user, sign = account.split(':') path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user)) resp = make_pre_authed_request(env, 'GET', path, agent=self.agent).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 = make_pre_authed_request(env, 'HEAD', path, agent=self.agent).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: 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 = make_pre_authed_request(env, 'GET', path, agent=self.agent).get_response(self.app) if resp.status_int // 100 != 2: return None detail = json.loads(resp.body) if detail['expires'] < time(): make_pre_authed_request(env, 'DELETE', path, agent=self.agent).get_response(self.app) return None groups = [g['name'] for g in detail['groups']] if '.admin' in groups: groups.remove('.admin') groups.append(detail['account_id']) groups = ','.join(groups) if memcache_client: memcache_client.set(memcache_key, (detail['expires'], groups), 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 webob.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 webob.Response). :param req: webob.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 = make_pre_authed_request(req.environ, req.method, path, agent=self.agent).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 webob.Request to process. :returns: webob.Response, 204 on success """ if not self.is_super_admin(req): return HTTPForbidden(request=req) path = quote('/v1/%s' % self.auth_account) resp = make_pre_authed_request(req.environ, 'PUT', path, agent=self.agent).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 = make_pre_authed_request(req.environ, 'PUT', path, agent=self.agent).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 = make_pre_authed_request(req.environ, 'PUT', path, agent=self.agent).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 webob.Request to process. :returns: webob.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 = make_pre_authed_request(req.environ, 'GET', path, agent=self.agent).get_response(self.app) if resp.status_int // 100 != 2: raise Exception('Could not list main auth account: %s %s' % (path, resp.status)) sublisting = json.loads(resp.body) if not sublisting: break for container in sublisting: if container['name'][0] != '.': listing.append({'name': container['name']}) marker = sublisting[-1]['name'].encode('utf-8') return Response(body=json.dumps({'accounts': listing})) def handle_get_account(self, req): """ Handles the GET v2/ call for getting account information. Can only be called by an account .admin. On success, a JSON dictionary will be returned containing the keys `account_id`, `services`, and `users`. The `account_id` is the value used when creating service accounts. The `services` value is a dict as described in the :func:`handle_get_token` call. The `users` value is a list of dicts, each dict representing a user and currently only containing the single key `name`. For example:: {"account_id": "AUTH_018c3946-23f8-4efb-a8fb-b67aae8e4162", "services": {"storage": {"default": "local", "local": "http://127.0.0.1:8080/v1/AUTH_018c3946"}}, "users": [{"name": "tester"}, {"name": "tester3"}]} :param req: The webob.Request to process. :returns: webob.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 = make_pre_authed_request(req.environ, 'GET', path, agent=self.agent).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 = make_pre_authed_request(req.environ, 'GET', path, agent=self.agent).get_response(self.app) if resp.status_int == 404: return HTTPNotFound(request=req) if resp.status_int // 100 != 2: raise Exception('Could not list in main auth account: %s %s' % (path, resp.status)) account_id = resp.headers['X-Container-Meta-Account-Id'] sublisting = json.loads(resp.body) if not sublisting: break for obj in sublisting: if obj['name'][0] != '.': listing.append({'name': obj['name']}) marker = sublisting[-1]['name'].encode('utf-8') return Response(body=json.dumps({'account_id': account_id, 'services': services, 'users': listing})) def handle_set_services(self, req): """ Handles the POST v2//.services call for setting services information. Can only be called by a reseller .admin. In the :func:`handle_get_account` (GET v2/) call, a section of the returned JSON dict is `services`. This section looks something like this:: "services": {"storage": {"default": "local", "local": "http://127.0.0.1:8080/v1/AUTH_018c3946"}} Making use of this section is described in :func:`handle_get_token`. This function allows setting values within this section for the , allowing the addition of new service end points or updating existing ones. The body of the POST request should contain a JSON dict with the following format:: {"service_name": {"end_point_name": "end_point_value"}} There can be multiple services and multiple end points in the same call. Any new services or end points will be added to the existing set of services and end points. Any existing services with the same service name will be merged with the new end points. Any existing end points with the same end point name will have their values updated. The updated services dictionary will be returned on success. :param req: The webob.Request to process. :returns: webob.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 = make_pre_authed_request(req.environ, 'GET', path, agent=self.agent).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 = make_pre_authed_request(req.environ, 'PUT', path, services, agent=self.agent).get_response(self.app) if resp.status_int // 100 != 2: raise Exception('Could not save .services object: %s %s' % (path, resp.status)) return Response(request=req, body=services) def handle_put_account(self, req): """ Handles the PUT v2/ call for adding an account to the auth system. Can only be called by a .reseller_admin. By default, a newly created UUID4 will be used with the reseller prefix as the account id used when creating corresponding service accounts. However, you can provide an X-Account-Suffix header to replace the UUID4 part. :param req: The webob.Request to process. :returns: webob.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 = make_pre_authed_request(req.environ, 'HEAD', path, agent=self.agent).get_response(self.app) if resp.status_int == 404: resp = make_pre_authed_request(req.environ, 'PUT', path, agent=self.agent).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)}) 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 = make_pre_authed_request(req.environ, 'PUT', path, account, agent=self.agent).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 = make_pre_authed_request(req.environ, 'PUT', path, json.dumps(services), agent=self.agent).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 = make_pre_authed_request(req.environ, 'POST', path, headers={'X-Container-Meta-Account-Id': '%s%s' % (self.reseller_prefix, account_suffix)}, agent=self.agent).get_response(self.app) if resp.status_int // 100 != 2: raise Exception('Could not record the account id on the account: ' '%s %s' % (path, resp.status)) return HTTPCreated(request=req) def handle_delete_account(self, req): """ Handles the DELETE v2/ call for removing an account from the auth system. Can only be called by a .reseller_admin. :param req: The webob.Request to process. :returns: webob.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 = make_pre_authed_request(req.environ, 'GET', path, agent=self.agent).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 = make_pre_authed_request(req.environ, 'GET', path, agent=self.agent).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 = make_pre_authed_request(req.environ, 'DELETE', path, agent=self.agent).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 = make_pre_authed_request(req.environ, 'DELETE', path, agent=self.agent).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 = make_pre_authed_request(req.environ, 'DELETE', path, agent=self.agent).get_response(self.app) if resp.status_int // 100 != 2 and resp.status_int != 404: raise Exception('Could not delete account marked: %s %s' % (path, resp.status)) return HTTPNoContent(request=req) def handle_get_user(self, req): """ Handles the GET v2// call for getting user information. Can only be called by an account .admin. On success, a JSON dict will be returned as described:: {"groups": [ # List of groups the user is a member of {"name": ":"}, # The first group is a unique user identifier {"name": ""}, # The second group is the auth account name {"name": ""} # There may be additional groups, .admin being a special # group indicating an account admin and .reseller_admin # indicating a reseller admin. ], "auth": "plaintext:" # The auth-type and key for the user; currently only plaintext is # implemented. } For example:: {"groups": [{"name": "test:tester"}, {"name": "test"}, {"name": ".admin"}], "auth": "plaintext:testing"} If the in the request is the special user `.groups`, the JSON dict will contain a single key of `groups` whose value is a list of dicts representing the active groups within the account. Each dict currently has the single key `name`. For example:: {"groups": [{"name": ".admin"}, {"name": "test"}, {"name": "test:tester"}, {"name": "test:tester3"}]} :param req: The webob.Request to process. :returns: webob.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 = make_pre_authed_request(req.environ, 'GET', path, agent=self.agent).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 = make_pre_authed_request(req.environ, 'GET', path, agent=self.agent).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 = make_pre_authed_request(req.environ, 'GET', path, agent=self.agent).get_response(self.app) if resp.status_int == 404: return HTTPNotFound(request=req) if resp.status_int // 100 != 2: raise Exception('Could not retrieve user object: %s %s' % (path, resp.status)) body = resp.body display_groups = [g['name'] for g in json.loads(body)['groups']] if ('.admin' in display_groups and not self.is_reseller_admin(req)) or \ ('.reseller_admin' in display_groups and not self.is_super_admin(req)): return HTTPForbidden(request=req) return Response(body=body) def handle_put_user(self, req): """ Handles the PUT v2// call for adding a user to an account. X-Auth-User-Key represents the user's key (url encoded), X-Auth-User-Admin may be set to `true` to create an account .admin, and X-Auth-User-Reseller-Admin may be set to `true` to create a .reseller_admin. Can only be called by an account .admin unless the user is to be a .reseller_admin, in which case the request must be by .super_admin. :param req: The webob.Request to process. :returns: webob.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 = make_pre_authed_request(req.environ, 'HEAD', path, agent=self.agent).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 = make_pre_authed_request(req.environ, 'PUT', path, json.dumps({'auth': auth_value, 'groups': [{'name': g} for g in groups]}), headers=headers, agent=self.agent).get_response(self.app) if resp.status_int == 404: return HTTPNotFound(request=req) if resp.status_int // 100 != 2: raise Exception('Could not create user object: %s %s' % (path, resp.status)) return HTTPCreated(request=req) def handle_delete_user(self, req): """ Handles the DELETE v2// call for deleting a user from an account. Can only be called by an account .admin. :param req: The webob.Request to process. :returns: webob.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 = make_pre_authed_request(req.environ, 'HEAD', path, agent=self.agent).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 = make_pre_authed_request(req.environ, 'DELETE', path, agent=self.agent).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 = make_pre_authed_request(req.environ, 'DELETE', path, agent=self.agent).get_response(self.app) if resp.status_int // 100 != 2 and resp.status_int != 404: raise Exception('Could not delete the user object: %s %s' % (path, resp.status)) return HTTPNoContent(request=req) def handle_get_token(self, req): """ Handles the various `request for token and service end point(s)` calls. There are various formats to support the various auth servers in the past. Examples:: GET /v1//auth X-Auth-User: : or X-Storage-User: X-Auth-Key: or X-Storage-Pass: GET /auth X-Auth-User: : or X-Storage-User: : X-Auth-Key: or X-Storage-Pass: GET /v1.0 X-Auth-User: : or X-Storage-User: : X-Auth-Key: or X-Storage-Pass: Values should be url encoded, "act%3Ausr" instead of "act:usr" for example; however, for backwards compatibility the colon may be included unencoded. On successful authentication, the response will have X-Auth-Token and X-Storage-Token set to the token to use with Swift and X-Storage-URL set to the URL to the default Swift cluster to use. The response body will be set to the account's services JSON object as described here:: {"storage": { # Represents the Swift storage service end points "default": "cluster1", # Indicates which cluster is the default "cluster1": "", # A Swift cluster that can be used with this account, # "cluster1" is the name of the cluster which is usually a # location indicator (like "dfw" for a datacenter region). "cluster2": "" # Another Swift cluster that can be used with this account, # there will always be at least one Swift cluster to use or # this whole "storage" dict won't be included at all. }, "servers": { # Represents the Nova server service end points # Expected to be similar to the "storage" dict, but not # implemented yet. }, # Possibly other service dicts, not implemented yet. } :param req: The webob.Request to process. :returns: webob.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 = make_pre_authed_request(req.environ, 'GET', path, agent=self.agent).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 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 = make_pre_authed_request(req.environ, 'GET', path, agent=self.agent).get_response(self.app) if resp.status_int // 100 == 2: token_detail = json.loads(resp.body) if token_detail['expires'] > time(): token = candidate_token else: make_pre_authed_request(req.environ, 'DELETE', path, agent=self.agent).get_response(self.app) elif resp.status_int != 404: raise Exception('Could not detect whether a token already ' 'exists: %s %s' % (path, resp.status)) # 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 = make_pre_authed_request(req.environ, 'HEAD', path, agent=self.agent).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)) resp = make_pre_authed_request(req.environ, 'PUT', path, json.dumps({'account': account, 'user': user, 'account_id': account_id, 'groups': user_detail['groups'], 'expires': time() + self.token_life}), agent=self.agent).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 = make_pre_authed_request(req.environ, 'POST', path, headers={'X-Object-Meta-Auth-Token': token}, agent=self.agent).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 = make_pre_authed_request(req.environ, 'GET', path, agent=self.agent).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-storage-url': url}) def handle_validate_token(self, req): """ Handles the GET v2/.token/ call for validating a token, usually called by a service like Swift. On a successful validation, X-Auth-TTL will be set for how much longer this token is valid and X-Auth-Groups will contain a comma separated list of groups the user belongs to. The first group listed will be a unique identifier for the user the token represents. .reseller_admin is a special group that indicates the user should be allowed to do anything on any account. :param req: The webob.Request to process. :returns: webob.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 = make_pre_authed_request(req.environ, 'GET', path, agent=self.agent).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(): make_pre_authed_request(req.environ, 'DELETE', path, agent=self.agent).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(): 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') 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 webob 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 = make_pre_authed_request(req.environ, 'GET', path, agent=self.agent).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 webob.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 webob.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 webob.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 gholt-swauth-3782d56/test_swauth/000077500000000000000000000000001173502047100167765ustar00rootroot00000000000000gholt-swauth-3782d56/test_swauth/__init__.py000066400000000000000000000004421173502047100211070ustar00rootroot00000000000000# 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) gholt-swauth-3782d56/test_swauth/unit/000077500000000000000000000000001173502047100177555ustar00rootroot00000000000000gholt-swauth-3782d56/test_swauth/unit/__init__.py000066400000000000000000000000001173502047100220540ustar00rootroot00000000000000gholt-swauth-3782d56/test_swauth/unit/test_authtypes.py000066400000000000000000000040621173502047100234160ustar00rootroot00000000000000# 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() gholt-swauth-3782d56/test_swauth/unit/test_middleware.py000066400000000000000000005070021173502047100235070ustar00rootroot00000000000000# 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 webob import Request, Response from swauth import middleware as auth from swauth.authtypes import MAX_TOKEN_LENGTH class FakeMemcache(object): def __init__(self): self.store = {} def get(self, key): return self.store.get(key) def set(self, key, value, timeout=0): self.store[key] = value return True def incr(self, key, timeout=0): self.store[key] = self.store.setdefault(key, 0) + 1 return self.store[key] @contextmanager def soft_lock(self, key, timeout=0, retries=5): 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'})(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_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_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_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.') if __name__ == '__main__': unittest.main() gholt-swauth-3782d56/webadmin/000077500000000000000000000000001173502047100162125ustar00rootroot00000000000000gholt-swauth-3782d56/webadmin/index.html000066400000000000000000000677151173502047100202270ustar00rootroot00000000000000
Swauth