pax_global_header 0000666 0000000 0000000 00000000064 11735020471 0014512 g ustar 00root root 0000000 0000000 52 comment=3782d56ce48867ef9130ced6873078073cf5162a
gholt-swauth-3782d56/ 0000775 0000000 0000000 00000000000 11735020471 0014424 5 ustar 00root root 0000000 0000000 gholt-swauth-3782d56/.gitignore 0000664 0000000 0000000 00000000036 11735020471 0016413 0 ustar 00root root 0000000 0000000 *.egg-info
*.py[co]
.DS_Store
gholt-swauth-3782d56/.unittests 0000775 0000000 0000000 00000000163 11735020471 0016472 0 ustar 00root root 0000000 0000000 #!/bin/bash
nosetests test_swauth/unit --exe --with-coverage --cover-package swauth --cover-erase
rm -f .coverage
gholt-swauth-3782d56/AUTHORS 0000664 0000000 0000000 00000001024 11735020471 0015471 0 ustar 00root root 0000000 0000000 Maintainer
----------
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/CHANGELOG 0000664 0000000 0000000 00000001544 11735020471 0015642 0 ustar 00root root 0000000 0000000 swauth (1.0.3-dev)
This release is still under development. A full change log will be made at
release. Until then, you can see what has changed with:
git log 1.0.2..HEAD
swauth (1.0.2)
Fixed bug rejecting requests when using multiple instances of Swauth or
Swauth with other auth services.
Fixed bug interpreting URL-encoded user names and keys.
Added support for the Swift container sync feature.
Allowed /not/ setting super_admin_key to disable Swauth administration
features.
Added swauth_remote mode so the Swauth middleware for one Swift cluster
could be pointing to the Swauth service on another Swift cluster, sharing
account/user data sets.
Added ability to purge stored tokens.
Added API documentation for internal Swauth API.
swauth (1.0.1)
Initial release after separation from Swift.
gholt-swauth-3782d56/LICENSE 0000664 0000000 0000000 00000026450 11735020471 0015440 0 ustar 00root root 0000000 0000000
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.in 0000664 0000000 0000000 00000000143 11735020471 0016160 0 ustar 00root root 0000000 0000000 include AUTHORS LICENSE .unittests test_swauth/__init__.py
include CHANGELOG
graft doc
graft etc
gholt-swauth-3782d56/README.md 0000664 0000000 0000000 00000003702 11735020471 0015705 0 ustar 00root root 0000000 0000000 Swauth
------
An Auth Service for Swift as WSGI Middleware that uses Swift itself as a
backing store. Sphinx-built docs at:
See also for the standard OpenStack
auth service.
NOTE
----
**Be sure to review the Sphinx-built docs at:
**
Quick Install
-------------
1) Install Swauth with ``sudo python setup.py install`` or ``sudo python
setup.py develop`` or via whatever packaging system you may be using.
2) Alter your proxy-server.conf pipeline to have swauth instead of tempauth:
Was:
[pipeline:main]
pipeline = catch_errors cache tempauth proxy-server
Change To:
[pipeline:main]
pipeline = catch_errors cache swauth proxy-server
3) Add to your proxy-server.conf the section for the Swauth WSGI filter:
[filter:swauth]
use = egg:swauth#swauth
set log_name = swauth
super_admin_key = swauthkey
4) Be sure your proxy server allows account management:
[app:proxy-server]
...
allow_account_management = true
5) Restart your proxy server ``swift-init proxy reload``
6) Initialize the Swauth backing store in Swift ``swauth-prep -K swauthkey``
7) Add an account/user ``swauth-add-user -A http://127.0.0.1:8080/auth/ -K
swauthkey -a test tester testing``
8) Ensure it works ``swift -A http://127.0.0.1:8080/auth/v1.0 -U test:tester -K
testing stat -v``
Web Admin Install
-----------------
1) If you installed from packages, you'll need to cd to the webadmin directory
the package installed. This is ``/usr/share/doc/python-swauth/webadmin``
with the Lucid packages. If you installed from source, you'll need to cd to
the webadmin directory in the source directory.
2) Upload the Web Admin files with ``swift -A http://127.0.0.1:8080/auth/v1.0
-U .super_admin:.super_admin -K swauthkey upload .webadmin .``
3) Open ``http://127.0.0.1:8080/auth/`` in your browser.
gholt-swauth-3782d56/babel.cfg 0000664 0000000 0000000 00000000021 11735020471 0016143 0 ustar 00root root 0000000 0000000 [python: **.py]
gholt-swauth-3782d56/bin/ 0000775 0000000 0000000 00000000000 11735020471 0015174 5 ustar 00root root 0000000 0000000 gholt-swauth-3782d56/bin/swauth-add-account 0000775 0000000 0000000 00000005542 11735020471 0020623 0 ustar 00root root 0000000 0000000 #!/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-user 0000775 0000000 0000000 00000010240 11735020471 0020134 0 ustar 00root root 0000000 0000000 #!/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-tokens 0000775 0000000 0000000 00000016711 11735020471 0021371 0 ustar 00root root 0000000 0000000 #!/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-account 0000775 0000000 0000000 00000004626 11735020471 0021337 0 ustar 00root root 0000000 0000000 #!/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-user 0000775 0000000 0000000 00000004646 11735020471 0020663 0 ustar 00root root 0000000 0000000 #!/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-list 0000775 0000000 0000000 00000006513 11735020471 0017413 0 ustar 00root root 0000000 0000000 #!/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-prep 0000775 0000000 0000000 00000004545 11735020471 0017411 0 ustar 00root root 0000000 0000000 #!/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-service 0000775 0000000 0000000 00000005510 11735020471 0022317 0 ustar 00root root 0000000 0000000 #!/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/ 0000775 0000000 0000000 00000000000 11735020471 0015171 5 ustar 00root root 0000000 0000000 gholt-swauth-3782d56/doc/build/ 0000775 0000000 0000000 00000000000 11735020471 0016270 5 ustar 00root root 0000000 0000000 gholt-swauth-3782d56/doc/build/.gitignore 0000664 0000000 0000000 00000000002 11735020471 0020250 0 ustar 00root root 0000000 0000000 *
gholt-swauth-3782d56/doc/source/ 0000775 0000000 0000000 00000000000 11735020471 0016471 5 ustar 00root root 0000000 0000000 gholt-swauth-3782d56/doc/source/_static/ 0000775 0000000 0000000 00000000000 11735020471 0020117 5 ustar 00root root 0000000 0000000 gholt-swauth-3782d56/doc/source/_static/.empty 0000664 0000000 0000000 00000000000 11735020471 0021244 0 ustar 00root root 0000000 0000000 gholt-swauth-3782d56/doc/source/_templates/ 0000775 0000000 0000000 00000000000 11735020471 0020626 5 ustar 00root root 0000000 0000000 gholt-swauth-3782d56/doc/source/_templates/.empty 0000664 0000000 0000000 00000000000 11735020471 0021753 0 ustar 00root root 0000000 0000000 gholt-swauth-3782d56/doc/source/api.rst 0000664 0000000 0000000 00000031126 11735020471 0017777 0 ustar 00root root 0000000 0000000 .. _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.rst 0000664 0000000 0000000 00000000252 11735020471 0021250 0 ustar 00root root 0000000 0000000 .. _swauth_authtypes_module:
swauth.authtypes
=================
.. automodule:: swauth.authtypes
:members:
:undoc-members:
:show-inheritance:
:noindex:
gholt-swauth-3782d56/doc/source/conf.py 0000664 0000000 0000000 00000016766 11735020471 0020010 0 ustar 00root root 0000000 0000000 # -*- 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.rst 0000664 0000000 0000000 00000015164 11735020471 0020657 0 ustar 00root root 0000000 0000000 ----------------------
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.rst 0000664 0000000 0000000 00000015735 11735020471 0020345 0 ustar 00root root 0000000 0000000 .. 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.rst 0000664 0000000 0000000 00000027351 11735020471 0020655 0 ustar 00root root 0000000 0000000 .. _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.rst 0000664 0000000 0000000 00000000237 11735020471 0021342 0 ustar 00root root 0000000 0000000 .. _swauth_middleware_module:
swauth.middleware
=================
.. automodule:: swauth.middleware
:members:
:undoc-members:
:show-inheritance:
gholt-swauth-3782d56/doc/source/swauth.rst 0000664 0000000 0000000 00000000163 11735020471 0020536 0 ustar 00root root 0000000 0000000 .. _swauth_module:
swauth
======
.. automodule:: swauth
:members:
:undoc-members:
:show-inheritance:
gholt-swauth-3782d56/etc/ 0000775 0000000 0000000 00000000000 11735020471 0015177 5 ustar 00root root 0000000 0000000 gholt-swauth-3782d56/etc/proxy-server.conf-sample 0000664 0000000 0000000 00000006277 11735020471 0022026 0 ustar 00root root 0000000 0000000 [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/ 0000775 0000000 0000000 00000000000 11735020471 0015663 5 ustar 00root root 0000000 0000000 gholt-swauth-3782d56/locale/swauth.pot 0000664 0000000 0000000 00000001525 11735020471 0017725 0 ustar 00root root 0000000 0000000 # 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.cfg 0000664 0000000 0000000 00000000573 11735020471 0016252 0 ustar 00root root 0000000 0000000 [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.py 0000664 0000000 0000000 00000005267 11735020471 0016150 0 ustar 00root root 0000000 0000000 #!/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/ 0000775 0000000 0000000 00000000000 11735020471 0015737 5 ustar 00root root 0000000 0000000 gholt-swauth-3782d56/swauth/__init__.py 0000664 0000000 0000000 00000001457 11735020471 0020057 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000007162 11735020471 0020345 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000207261 11735020471 0020436 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 11735020471 0016776 5 ustar 00root root 0000000 0000000 gholt-swauth-3782d56/test_swauth/__init__.py 0000664 0000000 0000000 00000000442 11735020471 0021107 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 11735020471 0017755 5 ustar 00root root 0000000 0000000 gholt-swauth-3782d56/test_swauth/unit/__init__.py 0000664 0000000 0000000 00000000000 11735020471 0022054 0 ustar 00root root 0000000 0000000 gholt-swauth-3782d56/test_swauth/unit/test_authtypes.py 0000664 0000000 0000000 00000004062 11735020471 0023416 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000507002 11735020471 0023507 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 11735020471 0016212 5 ustar 00root root 0000000 0000000 gholt-swauth-3782d56/webadmin/index.html 0000664 0000000 0000000 00000067715 11735020471 0020227 0 ustar 00root root 0000000 0000000