Merge lp:~gholt/swift/swauth2 into lp:~hudson-openstack/swift/trunk

Proposed by gholt
Status: Merged
Approved by: Chuck Thier
Approved revision: 168
Merged at revision: 157
Proposed branch: lp:~gholt/swift/swauth2
Merge into: lp:~hudson-openstack/swift/trunk
Diff against target: 6032 lines (+5515/-49)
31 files modified
bin/swauth-add-account (+67/-0)
bin/swauth-add-user (+92/-0)
bin/swauth-cleanup-tokens (+104/-0)
bin/swauth-delete-account (+59/-0)
bin/swauth-delete-user (+59/-0)
bin/swauth-list (+85/-0)
bin/swauth-prep (+58/-0)
bin/swauth-set-account-service (+72/-0)
bin/swift-auth-to-swauth (+46/-0)
doc/source/admin_guide.rst (+18/-0)
doc/source/deployment_guide.rst (+37/-0)
doc/source/development_auth.rst (+3/-2)
doc/source/development_saio.rst (+35/-8)
doc/source/howto_cyberduck.rst (+3/-1)
doc/source/howto_installmultinode.rst (+59/-8)
doc/source/misc.rst (+9/-0)
doc/source/overview_auth.rst (+145/-6)
etc/auth-server.conf-sample (+1/-0)
etc/proxy-server.conf-sample (+35/-1)
etc/stats.conf-sample (+3/-0)
setup.py (+6/-0)
swift/common/middleware/auth.py (+5/-3)
swift/common/middleware/swauth.py (+1312/-0)
swift/common/utils.py (+1/-0)
swift/proxy/server.py (+2/-1)
test/functional/sample.conf (+5/-0)
test/functional/swift.py (+4/-3)
test/functionalnosetests/swift_testing.py (+4/-1)
test/probe/common.py (+35/-15)
test/unit/common/middleware/test_auth.py (+34/-0)
test/unit/common/middleware/test_swauth.py (+3117/-0)
To merge this branch: bzr merge lp:~gholt/swift/swauth2
Reviewer Review Type Date Requested Status
Chuck Thier (community) Approve
Greg Lange (community) Approve
Review via email: mp+43312@code.launchpad.net

Commit message

Incorporated Swauth into Swift as an optional DevAuth replacement.

Description of the change

Incorporated Swauth into Swift as an optional DevAuth replacement.

The best place to start looking at this is to build the docs and read doc/build/html/overview_auth.html

New things:

- Scalable. As scalable as Swift itself. web+scale2.0
- List accounts.
- List users.
- List groups.
- Delete accounts.
- Delete users.
- Update account service end points.
- Update users.
- Preliminary support for multiple clusters and services under one auth.

To switch to Swauth from DevAuth on an SAIO:

$ resetswift
$ mv /etc/swift/auth-server.conf /etc/swift/auth-server.conf.bak

Edit /etc/swift/proxy-server.conf:
    Change 'auth' in your pipeline to 'swauth'
    Add the following section:
        [filter:swauth]
        use = egg:swift#swauth
        super_admin_key = swauthkey

Edit ~/bin/startmain and comment out 'swift-init auth-server start'

Edit ~/bin/startrest and comment out
                          'swift-auth-recreate-accounts -K devauth'

Create ~/bin/recreateaccounts:
    #!/bin/bash

    # Replace swauthkey with whatever your super_admin_key is
    # (recorded in /etc/swift/proxy-server.conf).
    swauth-prep -K swauthkey
    swauth-add-user -K swauthkey -a test tester testing
    swauth-add-user -K swauthkey -a test2 tester2 testing2
    swauth-add-user -K swauthkey test tester3 testing3
    swauth-add-user -K swauthkey -a -r reseller reseller reseller

Edit /etc/swift/func_test.conf:
    Change auth_port to 8080
    Add 'auth_prefix = /auth/'

$ startmain
$ recreateaccounts
$ ./.functests
$ ./.probetests

If you just really, really have to have your exact accounts from your old auth.db, you can use the swift-auth-to-swauth script.

To post a comment you must log in.
lp:~gholt/swift/swauth2 updated
151. By gholt

Merge from trunk

152. By gholt

swauth: Fixed unit tests for webob changes

153. By gholt

Merge from trunk

154. By gholt

Merge from trunk (i18n)

155. By gholt

i18nify log message

156. By gholt

Updated the docs to better reflect the .token_[0-f] container selection.

157. By gholt

Merge from trunk

158. By gholt

Removed extraneous print

159. By gholt

Merge from trunk

160. By gholt

Fix to limit account DELETEs to just reseller admins

161. By gholt

Fix bug where trying to access an account name that exactly matched the reseller_prefix would raise an exception

162. By gholt

Make Swauth return 404 on split_path exceptions

163. By gholt

Test updates suggested by glange

Revision history for this message
Greg Lange (greglange) wrote :

Looks good to me.

review: Approve
lp:~gholt/swift/swauth2 updated
164. By gholt

More error reporting

165. By gholt

Merge from trunk

166. By gholt

Added public/private urls for swauth default swift cluster setting

167. By gholt

Fixed bug with using new internal url

168. By gholt

Fp leak fix

Revision history for this message
Chuck Thier (cthier) wrote :

Looks good
Long live swauth!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'bin/swauth-add-account'
2--- bin/swauth-add-account 1970-01-01 00:00:00 +0000
3+++ bin/swauth-add-account 2011-01-10 20:41:38 +0000
4@@ -0,0 +1,67 @@
5+#!/usr/bin/python
6+# Copyright (c) 2010 OpenStack, LLC.
7+#
8+# Licensed under the Apache License, Version 2.0 (the "License");
9+# you may not use this file except in compliance with the License.
10+# You may obtain a copy of the License at
11+#
12+# http://www.apache.org/licenses/LICENSE-2.0
13+#
14+# Unless required by applicable law or agreed to in writing, software
15+# distributed under the License is distributed on an "AS IS" BASIS,
16+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
17+# implied.
18+# See the License for the specific language governing permissions and
19+# limitations under the License.
20+
21+import gettext
22+from optparse import OptionParser
23+from os.path import basename
24+from sys import argv, exit
25+from urlparse import urlparse
26+
27+from swift.common.bufferedhttp import http_connect_raw as http_connect
28+
29+
30+if __name__ == '__main__':
31+ gettext.install('swift', unicode=1)
32+ parser = OptionParser(usage='Usage: %prog [options] <account>')
33+ parser.add_option('-s', '--suffix', dest='suffix',
34+ default='', help='The suffix to use with the reseller prefix as the '
35+ 'storage account name (default: <randomly-generated-uuid4>) Note: If '
36+ 'the account already exists, this will have no effect on existing '
37+ 'service URLs. Those will need to be updated with '
38+ 'swauth-set-account-service')
39+ parser.add_option('-A', '--admin-url', dest='admin_url',
40+ default='http://127.0.0.1:8080/auth/', help='The URL to the auth '
41+ 'subsystem (default: http://127.0.0.1:8080/auth/)')
42+ parser.add_option('-U', '--admin-user', dest='admin_user',
43+ default='.super_admin', help='The user with admin rights to add users '
44+ '(default: .super_admin).')
45+ parser.add_option('-K', '--admin-key', dest='admin_key',
46+ help='The key for the user with admin rights to add users.')
47+ args = argv[1:]
48+ if not args:
49+ args.append('-h')
50+ (options, args) = parser.parse_args(args)
51+ if len(args) != 1:
52+ parser.parse_args(['-h'])
53+ account = args[0]
54+ parsed = urlparse(options.admin_url)
55+ if parsed.scheme not in ('http', 'https'):
56+ raise Exception('Cannot handle protocol scheme %s for url %s' %
57+ (parsed.scheme, repr(options.admin_url)))
58+ if not parsed.path:
59+ parsed.path = '/'
60+ elif parsed.path[-1] != '/':
61+ parsed.path += '/'
62+ path = '%sv2/%s' % (parsed.path, account)
63+ headers = {'X-Auth-Admin-User': options.admin_user,
64+ 'X-Auth-Admin-Key': options.admin_key}
65+ if options.suffix:
66+ headers['X-Account-Suffix'] = options.suffix
67+ conn = http_connect(parsed.hostname, parsed.port, 'PUT', path, headers,
68+ ssl=(parsed.scheme == 'https'))
69+ resp = conn.getresponse()
70+ if resp.status // 100 != 2:
71+ print 'Account creation failed: %s %s' % (resp.status, resp.reason)
72
73=== added file 'bin/swauth-add-user'
74--- bin/swauth-add-user 1970-01-01 00:00:00 +0000
75+++ bin/swauth-add-user 2011-01-10 20:41:38 +0000
76@@ -0,0 +1,92 @@
77+#!/usr/bin/python
78+# Copyright (c) 2010 OpenStack, LLC.
79+#
80+# Licensed under the Apache License, Version 2.0 (the "License");
81+# you may not use this file except in compliance with the License.
82+# You may obtain a copy of the License at
83+#
84+# http://www.apache.org/licenses/LICENSE-2.0
85+#
86+# Unless required by applicable law or agreed to in writing, software
87+# distributed under the License is distributed on an "AS IS" BASIS,
88+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
89+# implied.
90+# See the License for the specific language governing permissions and
91+# limitations under the License.
92+
93+import gettext
94+from optparse import OptionParser
95+from os.path import basename
96+from sys import argv, exit
97+from urlparse import urlparse
98+
99+from swift.common.bufferedhttp import http_connect_raw as http_connect
100+
101+
102+if __name__ == '__main__':
103+ gettext.install('swift', unicode=1)
104+ parser = OptionParser(
105+ usage='Usage: %prog [options] <account> <user> <password>')
106+ parser.add_option('-a', '--admin', dest='admin', action='store_true',
107+ default=False, help='Give the user administrator access; otherwise '
108+ 'the user will only have access to containers specifically allowed '
109+ 'with ACLs.')
110+ parser.add_option('-r', '--reseller-admin', dest='reseller_admin',
111+ action='store_true', default=False, help='Give the user full reseller '
112+ 'administrator access, giving them full access to all accounts within '
113+ 'the reseller, including the ability to create new accounts. Creating '
114+ 'a new reseller admin requires super_admin rights.')
115+ parser.add_option('-s', '--suffix', dest='suffix',
116+ default='', help='The suffix to use with the reseller prefix as the '
117+ 'storage account name (default: <randomly-generated-uuid4>) Note: If '
118+ 'the account already exists, this will have no effect on existing '
119+ 'service URLs. Those will need to be updated with '
120+ 'swauth-set-account-service')
121+ parser.add_option('-A', '--admin-url', dest='admin_url',
122+ default='http://127.0.0.1:8080/auth/', help='The URL to the auth '
123+ 'subsystem (default: http://127.0.0.1:8080/auth/')
124+ parser.add_option('-U', '--admin-user', dest='admin_user',
125+ default='.super_admin', help='The user with admin rights to add users '
126+ '(default: .super_admin).')
127+ parser.add_option('-K', '--admin-key', dest='admin_key',
128+ help='The key for the user with admin rights to add users.')
129+ args = argv[1:]
130+ if not args:
131+ args.append('-h')
132+ (options, args) = parser.parse_args(args)
133+ if len(args) != 3:
134+ parser.parse_args(['-h'])
135+ account, user, password = args
136+ parsed = urlparse(options.admin_url)
137+ if parsed.scheme not in ('http', 'https'):
138+ raise Exception('Cannot handle protocol scheme %s for url %s' %
139+ (parsed.scheme, repr(options.admin_url)))
140+ if not parsed.path:
141+ parsed.path = '/'
142+ elif parsed.path[-1] != '/':
143+ parsed.path += '/'
144+ # Ensure the account exists
145+ path = '%sv2/%s' % (parsed.path, account)
146+ headers = {'X-Auth-Admin-User': options.admin_user,
147+ 'X-Auth-Admin-Key': options.admin_key}
148+ if options.suffix:
149+ headers['X-Account-Suffix'] = options.suffix
150+ conn = http_connect(parsed.hostname, parsed.port, 'PUT', path, headers,
151+ ssl=(parsed.scheme == 'https'))
152+ resp = conn.getresponse()
153+ if resp.status // 100 != 2:
154+ print 'Account creation failed: %s %s' % (resp.status, resp.reason)
155+ # Add the user
156+ path = '%sv2/%s/%s' % (parsed.path, account, user)
157+ headers = {'X-Auth-Admin-User': options.admin_user,
158+ 'X-Auth-Admin-Key': options.admin_key,
159+ 'X-Auth-User-Key': password}
160+ if options.admin:
161+ headers['X-Auth-User-Admin'] = 'true'
162+ if options.reseller_admin:
163+ headers['X-Auth-User-Reseller-Admin'] = 'true'
164+ conn = http_connect(parsed.hostname, parsed.port, 'PUT', path, headers,
165+ ssl=(parsed.scheme == 'https'))
166+ resp = conn.getresponse()
167+ if resp.status // 100 != 2:
168+ print 'User creation failed: %s %s' % (resp.status, resp.reason)
169
170=== added file 'bin/swauth-cleanup-tokens'
171--- bin/swauth-cleanup-tokens 1970-01-01 00:00:00 +0000
172+++ bin/swauth-cleanup-tokens 2011-01-10 20:41:38 +0000
173@@ -0,0 +1,104 @@
174+#!/usr/bin/python
175+# Copyright (c) 2010 OpenStack, LLC.
176+#
177+# Licensed under the Apache License, Version 2.0 (the "License");
178+# you may not use this file except in compliance with the License.
179+# You may obtain a copy of the License at
180+#
181+# http://www.apache.org/licenses/LICENSE-2.0
182+#
183+# Unless required by applicable law or agreed to in writing, software
184+# distributed under the License is distributed on an "AS IS" BASIS,
185+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
186+# implied.
187+# See the License for the specific language governing permissions and
188+# limitations under the License.
189+
190+try:
191+ import simplejson as json
192+except ImportError:
193+ import json
194+import gettext
195+import re
196+from datetime import datetime, timedelta
197+from optparse import OptionParser
198+from sys import argv, exit
199+from time import sleep, time
200+
201+from swift.common.client import Connection
202+
203+
204+if __name__ == '__main__':
205+ gettext.install('swift', unicode=1)
206+ parser = OptionParser(usage='Usage: %prog [options]')
207+ parser.add_option('-t', '--token-life', dest='token_life',
208+ default='86400', help='The expected life of tokens; token objects '
209+ 'modified more than this number of seconds ago will be checked for '
210+ 'expiration (default: 86400).')
211+ parser.add_option('-s', '--sleep', dest='sleep',
212+ default='0.1', help='The number of seconds to sleep between token '
213+ 'checks (default: 0.1)')
214+ parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
215+ default=False, help='Outputs everything done instead of just the '
216+ 'deletions.')
217+ parser.add_option('-A', '--admin-url', dest='admin_url',
218+ default='http://127.0.0.1:8080/auth/', help='The URL to the auth '
219+ 'subsystem (default: http://127.0.0.1:8080/auth/)')
220+ parser.add_option('-K', '--admin-key', dest='admin_key',
221+ help='The key for .super_admin.')
222+ args = argv[1:]
223+ if not args:
224+ args.append('-h')
225+ (options, args) = parser.parse_args(args)
226+ if len(args) != 0:
227+ parser.parse_args(['-h'])
228+ options.admin_url = options.admin_url.rstrip('/')
229+ if not options.admin_url.endswith('/v1.0'):
230+ options.admin_url += '/v1.0'
231+ options.admin_user = '.super_admin:.super_admin'
232+ options.token_life = timedelta(0, float(options.token_life))
233+ options.sleep = float(options.sleep)
234+ conn = Connection(options.admin_url, options.admin_user, options.admin_key)
235+ for x in xrange(16):
236+ container = '.token_%x' % x
237+ marker = None
238+ while True:
239+ if options.verbose:
240+ print 'GET %s?marker=%s' % (container, marker)
241+ objs = conn.get_container(container, marker=marker)[1]
242+ if objs:
243+ marker = objs[-1]['name']
244+ else:
245+ if options.verbose:
246+ print 'No more objects in %s' % container
247+ break
248+ for obj in objs:
249+ last_modified = datetime(*map(int, re.split('[^\d]',
250+ obj['last_modified'])[:-1]))
251+ ago = datetime.utcnow() - last_modified
252+ if ago > options.token_life:
253+ if options.verbose:
254+ print '%s/%s last modified %ss ago; investigating' % \
255+ (container, obj['name'],
256+ ago.days * 86400 + ago.seconds)
257+ print 'GET %s/%s' % (container, obj['name'])
258+ detail = conn.get_object(container, obj['name'])[1]
259+ detail = json.loads(detail)
260+ if detail['expires'] < time():
261+ if options.verbose:
262+ print '%s/%s expired %ds ago; deleting' % \
263+ (container, obj['name'],
264+ time() - detail['expires'])
265+ print 'DELETE %s/%s' % (container, obj['name'])
266+ conn.delete_object(container, obj['name'])
267+ elif options.verbose:
268+ print "%s/%s won't expire for %ds; skipping" % \
269+ (container, obj['name'],
270+ detail['expires'] - time())
271+ elif options.verbose:
272+ print '%s/%s last modified %ss ago; skipping' % \
273+ (container, obj['name'],
274+ ago.days * 86400 + ago.seconds)
275+ sleep(options.sleep)
276+ if options.verbose:
277+ print 'Done.'
278
279=== added file 'bin/swauth-delete-account'
280--- bin/swauth-delete-account 1970-01-01 00:00:00 +0000
281+++ bin/swauth-delete-account 2011-01-10 20:41:38 +0000
282@@ -0,0 +1,59 @@
283+#!/usr/bin/python
284+# Copyright (c) 2010 OpenStack, LLC.
285+#
286+# Licensed under the Apache License, Version 2.0 (the "License");
287+# you may not use this file except in compliance with the License.
288+# You may obtain a copy of the License at
289+#
290+# http://www.apache.org/licenses/LICENSE-2.0
291+#
292+# Unless required by applicable law or agreed to in writing, software
293+# distributed under the License is distributed on an "AS IS" BASIS,
294+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
295+# implied.
296+# See the License for the specific language governing permissions and
297+# limitations under the License.
298+
299+import gettext
300+from optparse import OptionParser
301+from os.path import basename
302+from sys import argv, exit
303+from urlparse import urlparse
304+
305+from swift.common.bufferedhttp import http_connect_raw as http_connect
306+
307+
308+if __name__ == '__main__':
309+ gettext.install('swift', unicode=1)
310+ parser = OptionParser(usage='Usage: %prog [options] <account>')
311+ parser.add_option('-A', '--admin-url', dest='admin_url',
312+ default='http://127.0.0.1:8080/auth/', help='The URL to the auth '
313+ 'subsystem (default: http://127.0.0.1:8080/auth/')
314+ parser.add_option('-U', '--admin-user', dest='admin_user',
315+ default='.super_admin', help='The user with admin rights to add users '
316+ '(default: .super_admin).')
317+ parser.add_option('-K', '--admin-key', dest='admin_key',
318+ help='The key for the user with admin rights to add users.')
319+ args = argv[1:]
320+ if not args:
321+ args.append('-h')
322+ (options, args) = parser.parse_args(args)
323+ if len(args) != 1:
324+ parser.parse_args(['-h'])
325+ account = args[0]
326+ parsed = urlparse(options.admin_url)
327+ if parsed.scheme not in ('http', 'https'):
328+ raise Exception('Cannot handle protocol scheme %s for url %s' %
329+ (parsed.scheme, repr(options.admin_url)))
330+ if not parsed.path:
331+ parsed.path = '/'
332+ elif parsed.path[-1] != '/':
333+ parsed.path += '/'
334+ path = '%sv2/%s' % (parsed.path, account)
335+ headers = {'X-Auth-Admin-User': options.admin_user,
336+ 'X-Auth-Admin-Key': options.admin_key}
337+ conn = http_connect(parsed.hostname, parsed.port, 'DELETE', path, headers,
338+ ssl=(parsed.scheme == 'https'))
339+ resp = conn.getresponse()
340+ if resp.status // 100 != 2:
341+ print 'Account deletion failed: %s %s' % (resp.status, resp.reason)
342
343=== added file 'bin/swauth-delete-user'
344--- bin/swauth-delete-user 1970-01-01 00:00:00 +0000
345+++ bin/swauth-delete-user 2011-01-10 20:41:38 +0000
346@@ -0,0 +1,59 @@
347+#!/usr/bin/python
348+# Copyright (c) 2010 OpenStack, LLC.
349+#
350+# Licensed under the Apache License, Version 2.0 (the "License");
351+# you may not use this file except in compliance with the License.
352+# You may obtain a copy of the License at
353+#
354+# http://www.apache.org/licenses/LICENSE-2.0
355+#
356+# Unless required by applicable law or agreed to in writing, software
357+# distributed under the License is distributed on an "AS IS" BASIS,
358+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
359+# implied.
360+# See the License for the specific language governing permissions and
361+# limitations under the License.
362+
363+import gettext
364+from optparse import OptionParser
365+from os.path import basename
366+from sys import argv, exit
367+from urlparse import urlparse
368+
369+from swift.common.bufferedhttp import http_connect_raw as http_connect
370+
371+
372+if __name__ == '__main__':
373+ gettext.install('swift', unicode=1)
374+ parser = OptionParser(usage='Usage: %prog [options] <account> <user>')
375+ parser.add_option('-A', '--admin-url', dest='admin_url',
376+ default='http://127.0.0.1:8080/auth/', help='The URL to the auth '
377+ 'subsystem (default: http://127.0.0.1:8080/auth/')
378+ parser.add_option('-U', '--admin-user', dest='admin_user',
379+ default='.super_admin', help='The user with admin rights to add users '
380+ '(default: .super_admin).')
381+ parser.add_option('-K', '--admin-key', dest='admin_key',
382+ help='The key for the user with admin rights to add users.')
383+ args = argv[1:]
384+ if not args:
385+ args.append('-h')
386+ (options, args) = parser.parse_args(args)
387+ if len(args) != 2:
388+ parser.parse_args(['-h'])
389+ account, user = args
390+ parsed = urlparse(options.admin_url)
391+ if parsed.scheme not in ('http', 'https'):
392+ raise Exception('Cannot handle protocol scheme %s for url %s' %
393+ (parsed.scheme, repr(options.admin_url)))
394+ if not parsed.path:
395+ parsed.path = '/'
396+ elif parsed.path[-1] != '/':
397+ parsed.path += '/'
398+ path = '%sv2/%s/%s' % (parsed.path, account, user)
399+ headers = {'X-Auth-Admin-User': options.admin_user,
400+ 'X-Auth-Admin-Key': options.admin_key}
401+ conn = http_connect(parsed.hostname, parsed.port, 'DELETE', path, headers,
402+ ssl=(parsed.scheme == 'https'))
403+ resp = conn.getresponse()
404+ if resp.status // 100 != 2:
405+ print 'User deletion failed: %s %s' % (resp.status, resp.reason)
406
407=== added file 'bin/swauth-list'
408--- bin/swauth-list 1970-01-01 00:00:00 +0000
409+++ bin/swauth-list 2011-01-10 20:41:38 +0000
410@@ -0,0 +1,85 @@
411+#!/usr/bin/python
412+# Copyright (c) 2010 OpenStack, LLC.
413+#
414+# Licensed under the Apache License, Version 2.0 (the "License");
415+# you may not use this file except in compliance with the License.
416+# You may obtain a copy of the License at
417+#
418+# http://www.apache.org/licenses/LICENSE-2.0
419+#
420+# Unless required by applicable law or agreed to in writing, software
421+# distributed under the License is distributed on an "AS IS" BASIS,
422+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
423+# implied.
424+# See the License for the specific language governing permissions and
425+# limitations under the License.
426+
427+try:
428+ import simplejson as json
429+except ImportError:
430+ import json
431+import gettext
432+from optparse import OptionParser
433+from os.path import basename
434+from sys import argv, exit
435+from urlparse import urlparse
436+
437+from swift.common.bufferedhttp import http_connect_raw as http_connect
438+
439+
440+if __name__ == '__main__':
441+ gettext.install('swift', unicode=1)
442+ parser = OptionParser(usage='''
443+Usage: %prog [options] [account] [user]
444+
445+If [account] and [user] are omitted, a list of accounts will be output.
446+
447+If [account] is included but not [user], an account's information will be
448+output, including a list of users within the account.
449+
450+If [account] and [user] are included, the user's information will be output,
451+including a list of groups the user belongs to.
452+
453+If the [user] is '.groups', the active groups for the account will be listed.
454+'''.strip())
455+ parser.add_option('-p', '--plain-text', dest='plain_text',
456+ action='store_true', default=False, help='Changes the output from '
457+ 'JSON to plain text. This will cause an account to list only the '
458+ 'users and a user to list only the groups.')
459+ parser.add_option('-A', '--admin-url', dest='admin_url',
460+ default='http://127.0.0.1:8080/auth/', help='The URL to the auth '
461+ 'subsystem (default: http://127.0.0.1:8080/auth/')
462+ parser.add_option('-U', '--admin-user', dest='admin_user',
463+ default='.super_admin', help='The user with admin rights to add users '
464+ '(default: .super_admin).')
465+ parser.add_option('-K', '--admin-key', dest='admin_key',
466+ help='The key for the user with admin rights to add users.')
467+ args = argv[1:]
468+ if not args:
469+ args.append('-h')
470+ (options, args) = parser.parse_args(args)
471+ if len(args) > 2:
472+ parser.parse_args(['-h'])
473+ parsed = urlparse(options.admin_url)
474+ if parsed.scheme not in ('http', 'https'):
475+ raise Exception('Cannot handle protocol scheme %s for url %s' %
476+ (parsed.scheme, repr(options.admin_url)))
477+ if not parsed.path:
478+ parsed.path = '/'
479+ elif parsed.path[-1] != '/':
480+ parsed.path += '/'
481+ path = '%sv2/%s' % (parsed.path, '/'.join(args))
482+ headers = {'X-Auth-Admin-User': options.admin_user,
483+ 'X-Auth-Admin-Key': options.admin_key}
484+ conn = http_connect(parsed.hostname, parsed.port, 'GET', path, headers,
485+ ssl=(parsed.scheme == 'https'))
486+ resp = conn.getresponse()
487+ if resp.status // 100 != 2:
488+ print 'List failed: %s %s' % (resp.status, resp.reason)
489+ body = resp.read()
490+ if options.plain_text:
491+ info = json.loads(body)
492+ for group in info[['accounts', 'users', 'groups'][len(args)]]:
493+ print group['name']
494+ else:
495+ print body
496
497=== added file 'bin/swauth-prep'
498--- bin/swauth-prep 1970-01-01 00:00:00 +0000
499+++ bin/swauth-prep 2011-01-10 20:41:38 +0000
500@@ -0,0 +1,58 @@
501+#!/usr/bin/python
502+# Copyright (c) 2010 OpenStack, LLC.
503+#
504+# Licensed under the Apache License, Version 2.0 (the "License");
505+# you may not use this file except in compliance with the License.
506+# You may obtain a copy of the License at
507+#
508+# http://www.apache.org/licenses/LICENSE-2.0
509+#
510+# Unless required by applicable law or agreed to in writing, software
511+# distributed under the License is distributed on an "AS IS" BASIS,
512+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
513+# implied.
514+# See the License for the specific language governing permissions and
515+# limitations under the License.
516+
517+import gettext
518+from optparse import OptionParser
519+from os.path import basename
520+from sys import argv, exit
521+from urlparse import urlparse
522+
523+from swift.common.bufferedhttp import http_connect_raw as http_connect
524+
525+
526+if __name__ == '__main__':
527+ gettext.install('swift', unicode=1)
528+ parser = OptionParser(usage='Usage: %prog [options]')
529+ parser.add_option('-A', '--admin-url', dest='admin_url',
530+ default='http://127.0.0.1:8080/auth/', help='The URL to the auth '
531+ 'subsystem (default: http://127.0.0.1:8080/auth/')
532+ parser.add_option('-U', '--admin-user', dest='admin_user',
533+ default='.super_admin', help='The user with admin rights to add users '
534+ '(default: .super_admin).')
535+ parser.add_option('-K', '--admin-key', dest='admin_key',
536+ help='The key for the user with admin rights to add users.')
537+ args = argv[1:]
538+ if not args:
539+ args.append('-h')
540+ (options, args) = parser.parse_args(args)
541+ if args:
542+ parser.parse_args(['-h'])
543+ parsed = urlparse(options.admin_url)
544+ if parsed.scheme not in ('http', 'https'):
545+ raise Exception('Cannot handle protocol scheme %s for url %s' %
546+ (parsed.scheme, repr(options.admin_url)))
547+ if not parsed.path:
548+ parsed.path = '/'
549+ elif parsed.path[-1] != '/':
550+ parsed.path += '/'
551+ path = '%sv2/.prep' % parsed.path
552+ headers = {'X-Auth-Admin-User': options.admin_user,
553+ 'X-Auth-Admin-Key': options.admin_key}
554+ conn = http_connect(parsed.hostname, parsed.port, 'POST', path, headers,
555+ ssl=(parsed.scheme == 'https'))
556+ resp = conn.getresponse()
557+ if resp.status // 100 != 2:
558+ print 'Auth subsystem prep failed: %s %s' % (resp.status, resp.reason)
559
560=== added file 'bin/swauth-set-account-service'
561--- bin/swauth-set-account-service 1970-01-01 00:00:00 +0000
562+++ bin/swauth-set-account-service 2011-01-10 20:41:38 +0000
563@@ -0,0 +1,72 @@
564+#!/usr/bin/python
565+# Copyright (c) 2010 OpenStack, LLC.
566+#
567+# Licensed under the Apache License, Version 2.0 (the "License");
568+# you may not use this file except in compliance with the License.
569+# You may obtain a copy of the License at
570+#
571+# http://www.apache.org/licenses/LICENSE-2.0
572+#
573+# Unless required by applicable law or agreed to in writing, software
574+# distributed under the License is distributed on an "AS IS" BASIS,
575+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
576+# implied.
577+# See the License for the specific language governing permissions and
578+# limitations under the License.
579+
580+try:
581+ import simplejson as json
582+except ImportError:
583+ import json
584+import gettext
585+from optparse import OptionParser
586+from os.path import basename
587+from sys import argv, exit
588+from urlparse import urlparse
589+
590+from swift.common.bufferedhttp import http_connect_raw as http_connect
591+
592+
593+if __name__ == '__main__':
594+ gettext.install('swift', unicode=1)
595+ parser = OptionParser(usage='''
596+Usage: %prog [options] <account> <service> <name> <value>
597+
598+Sets a service URL for an account. Can only be set by a reseller admin.
599+
600+Example: %prog -K swauthkey test storage local http://127.0.0.1:8080/v1/AUTH_018c3946-23f8-4efb-a8fb-b67aae8e4162
601+'''.strip())
602+ parser.add_option('-A', '--admin-url', dest='admin_url',
603+ default='http://127.0.0.1:8080/auth/', help='The URL to the auth '
604+ 'subsystem (default: http://127.0.0.1:8080/auth/)')
605+ parser.add_option('-U', '--admin-user', dest='admin_user',
606+ default='.super_admin', help='The user with admin rights to add users '
607+ '(default: .super_admin).')
608+ parser.add_option('-K', '--admin-key', dest='admin_key',
609+ help='The key for the user with admin rights to add users.')
610+ args = argv[1:]
611+ if not args:
612+ args.append('-h')
613+ (options, args) = parser.parse_args(args)
614+ if len(args) != 4:
615+ parser.parse_args(['-h'])
616+ account, service, name, url = args
617+ parsed = urlparse(options.admin_url)
618+ if parsed.scheme not in ('http', 'https'):
619+ raise Exception('Cannot handle protocol scheme %s for url %s' %
620+ (parsed.scheme, repr(options.admin_url)))
621+ if not parsed.path:
622+ parsed.path = '/'
623+ elif parsed.path[-1] != '/':
624+ parsed.path += '/'
625+ path = '%sv2/%s/.services' % (parsed.path, account)
626+ body = json.dumps({service: {name: url}})
627+ headers = {'Content-Length': str(len(body)),
628+ 'X-Auth-Admin-User': options.admin_user,
629+ 'X-Auth-Admin-Key': options.admin_key}
630+ conn = http_connect(parsed.hostname, parsed.port, 'POST', path, headers,
631+ ssl=(parsed.scheme == 'https'))
632+ conn.send(body)
633+ resp = conn.getresponse()
634+ if resp.status // 100 != 2:
635+ print 'Service set failed: %s %s' % (resp.status, resp.reason)
636
637=== added file 'bin/swift-auth-to-swauth'
638--- bin/swift-auth-to-swauth 1970-01-01 00:00:00 +0000
639+++ bin/swift-auth-to-swauth 2011-01-10 20:41:38 +0000
640@@ -0,0 +1,46 @@
641+#!/usr/bin/python
642+# Copyright (c) 2010 OpenStack, LLC.
643+#
644+# Licensed under the Apache License, Version 2.0 (the "License");
645+# you may not use this file except in compliance with the License.
646+# You may obtain a copy of the License at
647+#
648+# http://www.apache.org/licenses/LICENSE-2.0
649+#
650+# Unless required by applicable law or agreed to in writing, software
651+# distributed under the License is distributed on an "AS IS" BASIS,
652+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
653+# implied.
654+# See the License for the specific language governing permissions and
655+# limitations under the License.
656+
657+import gettext
658+from subprocess import call
659+from sys import argv, exit
660+
661+import sqlite3
662+
663+
664+if __name__ == '__main__':
665+ gettext.install('swift', unicode=1)
666+ if len(argv) != 4 or argv[1] != '-K':
667+ exit('Syntax: %s -K <super_admin_key> <path to auth.db>' % argv[0])
668+ _, _, super_admin_key, auth_db = argv
669+ call(['swauth-prep', '-K', super_admin_key])
670+ conn = sqlite3.connect(auth_db)
671+ for account, cfaccount, user, password, admin, reseller_admin in \
672+ conn.execute('SELECT account, cfaccount, user, password, admin, '
673+ 'reseller_admin FROM account'):
674+ cmd = ['swauth-add-user', '-K', super_admin_key, '-s',
675+ cfaccount.split('_', 1)[1]]
676+ if admin == 't':
677+ cmd.append('-a')
678+ if reseller_admin == 't':
679+ cmd.append('-r')
680+ cmd.extend([account, user, password])
681+ print ' '.join(cmd)
682+ call(cmd)
683+ print '----------------------------------------------------------------'
684+ print ' Assuming the above worked perfectly, you should copy and paste '
685+ print ' those lines into your ~/bin/recreateaccounts script.'
686+ print '----------------------------------------------------------------'
687
688=== modified file 'doc/source/admin_guide.rst'
689--- doc/source/admin_guide.rst 2010-11-30 20:15:41 +0000
690+++ doc/source/admin_guide.rst 2011-01-10 20:41:38 +0000
691@@ -164,7 +164,10 @@
692 /etc/swift/stats.conf. Example conf file::
693
694 [stats]
695+ # For DevAuth:
696 auth_url = http://saio:11000/v1.0
697+ # For Swauth:
698+ # auth_url = http://saio:11000/auth/v1.0
699 auth_user = test:tester
700 auth_key = testing
701
702@@ -229,6 +232,21 @@
703 timings are dumped into a CSV file (/etc/swift/stats.csv by default) and can
704 then be graphed to see how cluster performance is trending.
705
706+------------------------------------
707+Additional Cleanup Script for Swauth
708+------------------------------------
709+
710+If you decide to use Swauth, you'll want to install a cronjob to clean up any
711+orphaned expired tokens. These orphaned tokens can occur when a "stampede"
712+occurs where a single user authenticates several times concurrently. Generally,
713+these orphaned tokens don't pose much of an issue, but it's good to clean them
714+up once a "token life" period (default: 1 day or 86400 seconds).
715+
716+This should be as simple as adding `swauth-cleanup-tokens -K swauthkey >
717+/dev/null` to a crontab entry on one of the proxies that is running Swauth; but
718+run `swauth-cleanup-tokens` with no arguments for detailed help on the options
719+available.
720+
721 ------------------------
722 Debugging Tips and Tools
723 ------------------------
724
725=== modified file 'doc/source/deployment_guide.rst'
726--- doc/source/deployment_guide.rst 2010-11-29 23:19:29 +0000
727+++ doc/source/deployment_guide.rst 2011-01-10 20:41:38 +0000
728@@ -484,6 +484,43 @@
729 node_timeout 10 Request timeout
730 ============ =================================== ========================
731
732+[swauth]
733+
734+===================== =============================== =======================
735+Option Default Description
736+--------------------- ------------------------------- -----------------------
737+use Entry point for
738+ paste.deploy to use for
739+ auth. To use the swauth
740+ set to:
741+ `egg:swift#swauth`
742+log_name auth-server Label used when logging
743+log_facility LOG_LOCAL0 Syslog log facility
744+log_level INFO Log level
745+log_headers True If True, log headers in
746+ each request
747+reseller_prefix AUTH The naming scope for the
748+ auth service. Swift
749+ storage accounts and
750+ auth tokens will begin
751+ with this prefix.
752+auth_prefix /auth/ The HTTP request path
753+ prefix for the auth
754+ service. Swift itself
755+ reserves anything
756+ beginning with the
757+ letter `v`.
758+default_swift_cluster local:http://127.0.0.1:8080/v1 The default Swift
759+ cluster to place newly
760+ created accounts on.
761+token_life 86400 The number of seconds a
762+ token is valid.
763+node_timeout 10 Request timeout
764+super_admin_key None The key for the
765+ .super_admin account.
766+===================== =============================== =======================
767+
768+
769 ------------------------
770 Memcached Considerations
771 ------------------------
772
773=== modified file 'doc/source/development_auth.rst'
774--- doc/source/development_auth.rst 2010-10-14 18:38:38 +0000
775+++ doc/source/development_auth.rst 2011-01-10 20:41:38 +0000
776@@ -8,7 +8,7 @@
777
778 The included swift/auth/server.py and swift/common/middleware/auth.py are good
779 minimal examples of how to create an external auth server and proxy server auth
780-middleware. Also, see the `Swauth <https://launchpad.net/swauth>`_ project for
781+middleware. Also, see swift/common/middleware/swauth.py for
782 a more complete implementation. The main points are that the auth middleware
783 can reject requests up front, before they ever get to the Swift Proxy
784 application, and afterwards when the proxy issues callbacks to verify
785@@ -356,6 +356,7 @@
786 self.auth_port = int(conf.get('port', 11000))
787 self.ssl = \
788 conf.get('ssl', 'false').lower() in ('true', 'on', '1', 'yes')
789+ self.auth_prefix = conf.get('prefix', '/')
790 self.timeout = int(conf.get('node_timeout', 10))
791
792 def authenticate(self, env, identity):
793@@ -371,7 +372,7 @@
794 return user
795 with Timeout(self.timeout):
796 conn = http_connect(self.auth_host, self.auth_port, 'GET',
797- '/token/%s' % token, ssl=self.ssl)
798+ '%stoken/%s' % (self.auth_prefix, token), ssl=self.ssl)
799 resp = conn.getresponse()
800 resp.read()
801 conn.close()
802
803=== modified file 'doc/source/development_saio.rst'
804--- doc/source/development_saio.rst 2010-11-30 23:37:31 +0000
805+++ doc/source/development_saio.rst 2011-01-10 20:41:38 +0000
806@@ -216,7 +216,9 @@
807
808 Sample configuration files are provided with all defaults in line-by-line comments.
809
810- #. Create `/etc/swift/auth-server.conf`::
811+ #. If your going to use the DevAuth (the default swift-auth-server), create
812+ `/etc/swift/auth-server.conf` (you can skip this if you're going to use
813+ Swauth)::
814
815 [DEFAULT]
816 user = <your-user-name>
817@@ -237,15 +239,25 @@
818 user = <your-user-name>
819
820 [pipeline:main]
821+ # For DevAuth:
822 pipeline = healthcheck cache auth proxy-server
823+ # For Swauth:
824+ # pipeline = healthcheck cache swauth proxy-server
825
826 [app:proxy-server]
827 use = egg:swift#proxy
828 allow_account_management = true
829
830+ # Only needed for DevAuth
831 [filter:auth]
832 use = egg:swift#auth
833
834+ # Only needed for Swauth
835+ [filter:swauth]
836+ use = egg:swift#swauth
837+ # Highly recommended to change this.
838+ super_admin_key = swauthkey
839+
840 [filter:healthcheck]
841 use = egg:swift#healthcheck
842
843@@ -562,18 +574,32 @@
844
845 #!/bin/bash
846
847+ # The auth-server line is only needed for DevAuth:
848 swift-init auth-server start
849 swift-init proxy-server start
850 swift-init account-server start
851 swift-init container-server start
852 swift-init object-server start
853
854+ #. For Swauth (not needed for DevAuth), create `~/bin/recreateaccounts`::
855+
856+ #!/bin/bash
857+
858+ # Replace devauth with whatever your super_admin key is (recorded in
859+ # /etc/swift/proxy-server.conf).
860+ swauth-prep -K swauthkey
861+ swauth-add-user -K swauthkey -a test tester testing
862+ swauth-add-user -K swauthkey -a test2 tester2 testing2
863+ swauth-add-user -K swauthkey test tester3 testing3
864+ swauth-add-user -K swauthkey -a -r reseller reseller reseller
865+
866 #. Create `~/bin/startrest`::
867
868 #!/bin/bash
869
870 # Replace devauth with whatever your super_admin key is (recorded in
871- # /etc/swift/auth-server.conf).
872+ # /etc/swift/auth-server.conf). This swift-auth-recreate-accounts line
873+ # is only needed for DevAuth:
874 swift-auth-recreate-accounts -K devauth
875 swift-init object-updater start
876 swift-init container-updater start
877@@ -589,13 +615,14 @@
878 #. `remakerings`
879 #. `cd ~/swift/trunk; ./.unittests`
880 #. `startmain` (The ``Unable to increase file descriptor limit. Running as non-root?`` warnings are expected and ok.)
881- #. `swift-auth-add-user -K devauth -a test tester testing` # Replace ``devauth`` with whatever your super_admin key is (recorded in /etc/swift/auth-server.conf).
882- #. Get an `X-Storage-Url` and `X-Auth-Token`: ``curl -v -H 'X-Storage-User: test:tester' -H 'X-Storage-Pass: testing' http://127.0.0.1:11000/v1.0``
883+ #. For Swauth: `recreateaccounts`
884+ #. For DevAuth: `swift-auth-add-user -K devauth -a test tester testing` # Replace ``devauth`` with whatever your super_admin key is (recorded in /etc/swift/auth-server.conf).
885+ #. Get an `X-Storage-Url` and `X-Auth-Token`: ``curl -v -H 'X-Storage-User: test:tester' -H 'X-Storage-Pass: testing' http://127.0.0.1:11000/v1.0`` # For Swauth, make the last URL `http://127.0.0.1:8080/auth/v1.0`
886 #. Check that you can GET account: ``curl -v -H 'X-Auth-Token: <token-from-x-auth-token-above>' <url-from-x-storage-url-above>``
887- #. Check that `st` works: `st -A http://127.0.0.1:11000/v1.0 -U test:tester -K testing stat`
888- #. `swift-auth-add-user -K devauth -a test2 tester2 testing2` # Replace ``devauth`` with whatever your super_admin key is (recorded in /etc/swift/auth-server.conf).
889- #. `swift-auth-add-user -K devauth test tester3 testing3` # Replace ``devauth`` with whatever your super_admin key is (recorded in /etc/swift/auth-server.conf).
890- #. `cp ~/swift/trunk/test/functional/sample.conf /etc/swift/func_test.conf`
891+ #. Check that `st` works: `st -A http://127.0.0.1:11000/v1.0 -U test:tester -K testing stat` # For Swauth, make the URL `http://127.0.0.1:8080/auth/v1.0`
892+ #. For DevAuth: `swift-auth-add-user -K devauth -a test2 tester2 testing2` # Replace ``devauth`` with whatever your super_admin key is (recorded in /etc/swift/auth-server.conf).
893+ #. For DevAuth: `swift-auth-add-user -K devauth test tester3 testing3` # Replace ``devauth`` with whatever your super_admin key is (recorded in /etc/swift/auth-server.conf).
894+ #. `cp ~/swift/trunk/test/functional/sample.conf /etc/swift/func_test.conf` # For Swauth, add auth_prefix = /auth/ and change auth_port = 8080.
895 #. `cd ~/swift/trunk; ./.functests` (Note: functional tests will first delete
896 everything in the configured accounts.)
897 #. `cd ~/swift/trunk; ./.probetests` (Note: probe tests will reset your
898
899=== modified file 'doc/source/howto_cyberduck.rst'
900--- doc/source/howto_cyberduck.rst 2010-09-12 00:23:24 +0000
901+++ doc/source/howto_cyberduck.rst 2011-01-10 20:41:38 +0000
902@@ -8,7 +8,9 @@
903
904 #. Install Swift, or have credentials for an existing Swift installation. If
905 you plan to install Swift on your own server, follow the general guidelines
906- in the section following this one.
907+ in the section following this one. (This documentation assumes the use of
908+ the DevAuth auth server; if you're using Swauth, you should change all auth
909+ URLs /v1.0 to /auth/v1.0)
910
911 #. Verify you can connect using the standard Swift Tool `st` from your
912 "public" URL (yes I know this resolves privately inside EC2)::
913
914=== modified file 'doc/source/howto_installmultinode.rst'
915--- doc/source/howto_installmultinode.rst 2010-11-29 23:52:51 +0000
916+++ doc/source/howto_installmultinode.rst 2011-01-10 20:41:38 +0000
917@@ -13,8 +13,8 @@
918 Basic architecture and terms
919 ----------------------------
920 - *node* - a host machine running one or more Swift services
921-- *Proxy node* - node that runs Proxy services
922-- *Auth node* - node that runs the Auth service
923+- *Proxy node* - node that runs Proxy services; can also run Swauth
924+- *Auth node* - node that runs the Auth service; only required for DevAuth
925 - *Storage node* - node that runs Account, Container, and Object services
926 - *ring* - a set of mappings of Swift data to physical devices
927
928@@ -23,13 +23,14 @@
929 - one Proxy node
930
931 - Runs the swift-proxy-server processes which proxy requests to the
932- appropriate Storage nodes.
933+ appropriate Storage nodes. For Swauth, the proxy server will also contain
934+ the Swauth service as WSGI middleware.
935
936 - one Auth node
937
938 - Runs the swift-auth-server which controls authentication and
939 authorization for all requests. This can be on the same node as a
940- Proxy node.
941+ Proxy node. This is only required for DevAuth.
942
943 - five Storage nodes
944
945@@ -120,16 +121,27 @@
946 user = swift
947
948 [pipeline:main]
949+ # For DevAuth:
950 pipeline = healthcheck cache auth proxy-server
951+ # For Swauth:
952+ # pipeline = healthcheck cache swauth proxy-server
953
954 [app:proxy-server]
955 use = egg:swift#proxy
956 allow_account_management = true
957
958+ # Only needed for DevAuth
959 [filter:auth]
960 use = egg:swift#auth
961 ssl = true
962
963+ # Only needed for Swauth
964+ [filter:swauth]
965+ use = egg:swift#swauth
966+ default_swift_cluster = https://<PROXY_LOCAL_NET_IP>:8080/v1
967+ # Highly recommended to change this key to something else!
968+ super_admin_key = swauthkey
969+
970 [filter:healthcheck]
971 use = egg:swift#healthcheck
972
973@@ -194,6 +206,8 @@
974 Configure the Auth node
975 -----------------------
976
977+.. note:: Only required for DevAuth; you can skip this section for Swauth.
978+
979 #. If this node is not running on the same node as a proxy, create a
980 self-signed cert as you did for the Proxy node
981
982@@ -358,13 +372,20 @@
983
984 You run these commands from the Auth node.
985
986+.. note:: For Swauth, replace the https://<AUTH_HOSTNAME>:11000/v1.0 with
987+ https://<PROXY_HOSTNAME>:8080/auth/v1.0
988+
989 #. Create a user with administrative privileges (account = system,
990 username = root, password = testpass). Make sure to replace
991- ``devauth`` with whatever super_admin key you assigned in the
992- auth-server.conf file above. *Note: None of the values of
993+ ``devauth`` (or ``swauthkey``) with whatever super_admin key you assigned in
994+ the auth-server.conf file (or proxy-server.conf file in the case of Swauth)
995+ above. *Note: None of the values of
996 account, username, or password are special - they can be anything.*::
997
998+ # For DevAuth:
999 swift-auth-add-user -K devauth -a system root testpass
1000+ # For Swauth:
1001+ swauth-add-user -K swauthkey -a system root testpass
1002
1003 #. Get an X-Storage-Url and X-Auth-Token::
1004
1005@@ -404,20 +425,50 @@
1006 use = egg:swift#memcache
1007 memcache_servers = <PROXY_LOCAL_NET_IP>:11211
1008
1009-#. Change the default_cluster_url to point to the load balanced url, rather than the first proxy server you created in /etc/swift/auth-server.conf::
1010+#. Change the default_cluster_url to point to the load balanced url, rather than the first proxy server you created in /etc/swift/auth-server.conf (for DevAuth) or in /etc/swift/proxy-server.conf (for Swauth)::
1011
1012+ # For DevAuth, in /etc/swift/auth-server.conf
1013 [app:auth-server]
1014 use = egg:swift#auth
1015 default_cluster_url = https://<LOAD_BALANCER_HOSTNAME>/v1
1016 # Highly recommended to change this key to something else!
1017 super_admin_key = devauth
1018
1019-#. After you change the default_cluster_url setting, you have to delete the auth database and recreate the Swift users, or manually update the auth database with the correct URL for each account.
1020+ # For Swauth, in /etc/swift/proxy-server.conf
1021+ [filter:swauth]
1022+ use = egg:swift#swauth
1023+ default_swift_cluster = local:http://<LOAD_BALANCER_HOSTNAME>/v1
1024+ # Highly recommended to change this key to something else!
1025+ super_admin_key = swauthkey
1026+
1027+#. For DevAuth, after you change the default_cluster_url setting, you have to delete the auth database and recreate the Swift users, or manually update the auth database with the correct URL for each account.
1028+
1029+ For Swauth, you can change a service URL with::
1030+
1031+ swauth-set-account-service -K swauthkey <account> storage local <new_url_for_the_account>
1032+
1033+ You can obtain old service URLs with::
1034+
1035+ swauth-list -K swauthkey <account>
1036
1037 #. Next, copy all the ring information to all the nodes, including your new proxy nodes, and ensure the ring info gets to all the storage nodes as well.
1038
1039 #. After you sync all the nodes, make sure the admin has the keys in /etc/swift and the ownership for the ring file is correct.
1040
1041+Additional Cleanup Script for Swauth
1042+------------------------------------
1043+
1044+If you decide to use Swauth, you'll want to install a cronjob to clean up any
1045+orphaned expired tokens. These orphaned tokens can occur when a "stampede"
1046+occurs where a single user authenticates several times concurrently. Generally,
1047+these orphaned tokens don't pose much of an issue, but it's good to clean them
1048+up once a "token life" period (default: 1 day or 86400 seconds).
1049+
1050+This should be as simple as adding `swauth-cleanup-tokens -K swauthkey >
1051+/dev/null` to a crontab entry on one of the proxies that is running Swauth; but
1052+run `swauth-cleanup-tokens` with no arguments for detailed help on the options
1053+available.
1054+
1055 Troubleshooting Notes
1056 ---------------------
1057 If you see problems, look in var/log/syslog (or messages on some distros).
1058
1059=== modified file 'doc/source/misc.rst'
1060--- doc/source/misc.rst 2010-10-13 15:50:11 +0000
1061+++ doc/source/misc.rst 2011-01-10 20:41:38 +0000
1062@@ -42,6 +42,15 @@
1063 :members:
1064 :show-inheritance:
1065
1066+.. _common_swauth:
1067+
1068+Swauth
1069+======
1070+
1071+.. automodule:: swift.common.middleware.swauth
1072+ :members:
1073+ :show-inheritance:
1074+
1075 .. _acls:
1076
1077 ACLs
1078
1079=== modified file 'doc/source/overview_auth.rst'
1080--- doc/source/overview_auth.rst 2010-09-06 03:30:09 +0000
1081+++ doc/source/overview_auth.rst 2011-01-10 20:41:38 +0000
1082@@ -48,9 +48,148 @@
1083
1084 Also, see :doc:`development_auth`.
1085
1086-------------------
1087-History and Future
1088-------------------
1089-
1090-What's established in Swift for authentication/authorization has history from
1091-before Swift, so that won't be recorded here.
1092+
1093+------
1094+Swauth
1095+------
1096+
1097+The Swauth system is an optional DevAuth replacement included at
1098+swift/common/middleware/swauth.py; a scalable authentication and
1099+authorization system that uses Swift itself as its backing store. This section
1100+will describe how it stores its data.
1101+
1102+At the topmost level, the auth system has its own Swift account it stores its
1103+own account information within. This Swift account is known as
1104+self.auth_account in the code and its name is in the format
1105+self.reseller_prefix + ".auth". In this text, we'll refer to this account as
1106+<auth_account>.
1107+
1108+The containers whose names do not begin with a period represent the accounts
1109+within the auth service. For example, the <auth_account>/test container would
1110+represent the "test" account.
1111+
1112+The objects within each container represent the users for that auth service
1113+account. For example, the <auth_account>/test/bob object would represent the
1114+user "bob" within the auth service account of "test". Each of these user
1115+objects contain a JSON dictionary of the format::
1116+
1117+ {"auth": "<auth_type>:<auth_value>", "groups": <groups_array>}
1118+
1119+The `<auth_type>` can only be `plaintext` at this time, and the `<auth_value>`
1120+is the plain text password itself.
1121+
1122+The `<groups_array>` contains at least two groups. The first is a unique group
1123+identifying that user and it's name is of the format `<user>:<account>`. The
1124+second group is the `<account>` itself. Additional groups of `.admin` for
1125+account administrators and `.reseller_admin` for reseller administrators may
1126+exist. Here's an example user JSON dictionary::
1127+
1128+ {"auth": "plaintext:testing",
1129+ "groups": ["name": "test:tester", "name": "test", "name": ".admin"]}
1130+
1131+To map an auth service account to a Swift storage account, the Service Account
1132+Id string is stored in the `X-Container-Meta-Account-Id` header for the
1133+<auth_account>/<account> container. To map back the other way, an
1134+<auth_account>/.account_id/<account_id> object is created with the contents of
1135+the corresponding auth service's account name.
1136+
1137+Also, to support a future where the auth service will support multiple Swift
1138+clusters or even multiple services for the same auth service account, an
1139+<auth_account>/<account>/.services object is created with its contents having a
1140+JSON dictionary of the format::
1141+
1142+ {"storage": {"default": "local", "local": <url>}}
1143+
1144+The "default" is always "local" right now, and "local" is always the single
1145+Swift cluster URL; but in the future there can be more than one cluster with
1146+various names instead of just "local", and the "default" key's value will
1147+contain the primary cluster to use for that account. Also, there may be more
1148+services in addition to the current "storage" service right now.
1149+
1150+Here's an example .services dictionary at the moment::
1151+
1152+ {"storage":
1153+ {"default": "local",
1154+ "local": "http://127.0.0.1:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9"}}
1155+
1156+But, here's an example of what the dictionary may look like in the future::
1157+
1158+ {"storage":
1159+ {"default": "dfw",
1160+ "dfw": "http://dfw.storage.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9",
1161+ "ord": "http://ord.storage.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9",
1162+ "sat": "http://ord.storage.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9"},
1163+ "servers":
1164+ {"default": "dfw",
1165+ "dfw": "http://dfw.servers.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9",
1166+ "ord": "http://ord.servers.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9",
1167+ "sat": "http://ord.servers.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9"}}
1168+
1169+Lastly, the tokens themselves are stored as objects in the
1170+`<auth_account>/.token_[0-f]` containers. The names of the objects are the
1171+token strings themselves, such as `AUTH_tked86bbd01864458aa2bd746879438d5a`.
1172+The exact `.token_[0-f]` container chosen is based on the final digit of the
1173+token name, such as `.token_a` for the token
1174+`AUTH_tked86bbd01864458aa2bd746879438d5a`. The contents of the token objects
1175+are JSON dictionaries of the format::
1176+
1177+ {"account": <account>,
1178+ "user": <user>,
1179+ "account_id": <account_id>,
1180+ "groups": <groups_array>,
1181+ "expires": <time.time() value>}
1182+
1183+The `<account>` is the auth service account's name for that token. The `<user>`
1184+is the user within the account for that token. The `<account_id>` is the
1185+same as the `X-Container-Meta-Account-Id` for the auth service's account,
1186+as described above. The `<groups_array>` is the user's groups, as described
1187+above with the user object. The "expires" value indicates when the token is no
1188+longer valid, as compared to Python's time.time() value.
1189+
1190+Here's an example token object's JSON dictionary::
1191+
1192+ {"account": "test",
1193+ "user": "tester",
1194+ "account_id": "AUTH_8980f74b1cda41e483cbe0a925f448a9",
1195+ "groups": ["name": "test:tester", "name": "test", "name": ".admin"],
1196+ "expires": 1291273147.1624689}
1197+
1198+To easily map a user to an already issued token, the token name is stored in
1199+the user object's `X-Object-Meta-Auth-Token` header.
1200+
1201+Here is an example full listing of an <auth_account>::
1202+
1203+ .account_id
1204+ AUTH_2282f516-559f-4966-b239-b5c88829e927
1205+ AUTH_f6f57a3c-33b5-4e85-95a5-a801e67505c8
1206+ AUTH_fea96a36-c177-4ca4-8c7e-b8c715d9d37b
1207+ .token_0
1208+ .token_1
1209+ .token_2
1210+ .token_3
1211+ .token_4
1212+ .token_5
1213+ .token_6
1214+ AUTH_tk9d2941b13d524b268367116ef956dee6
1215+ .token_7
1216+ .token_8
1217+ AUTH_tk93627c6324c64f78be746f1e6a4e3f98
1218+ .token_9
1219+ .token_a
1220+ .token_b
1221+ .token_c
1222+ .token_d
1223+ .token_e
1224+ AUTH_tk0d37d286af2c43ffad06e99112b3ec4e
1225+ .token_f
1226+ AUTH_tk766bbde93771489982d8dc76979d11cf
1227+ reseller
1228+ .services
1229+ reseller
1230+ test
1231+ .services
1232+ tester
1233+ tester3
1234+ test2
1235+ .services
1236+ tester2
1237
1238=== modified file 'etc/auth-server.conf-sample'
1239--- etc/auth-server.conf-sample 2010-09-10 20:40:43 +0000
1240+++ etc/auth-server.conf-sample 2011-01-10 20:41:38 +0000
1241@@ -1,3 +1,4 @@
1242+# Only needed for DevAuth; Swauth is within the proxy-server.conf
1243 [DEFAULT]
1244 # bind_ip = 0.0.0.0
1245 # bind_port = 11000
1246
1247=== modified file 'etc/proxy-server.conf-sample'
1248--- etc/proxy-server.conf-sample 2010-11-29 23:19:29 +0000
1249+++ etc/proxy-server.conf-sample 2011-01-10 20:41:38 +0000
1250@@ -9,7 +9,10 @@
1251 # key_file = /etc/swift/proxy.key
1252
1253 [pipeline:main]
1254+# For DevAuth:
1255 pipeline = catch_errors healthcheck cache ratelimit auth proxy-server
1256+# For Swauth:
1257+# pipeline = catch_errors healthcheck cache ratelimit swauth proxy-server
1258
1259 [app:proxy-server]
1260 use = egg:swift#proxy
1261@@ -33,6 +36,7 @@
1262 # 'false' no one, even authorized, can.
1263 # allow_account_management = false
1264
1265+# Only needed for DevAuth
1266 [filter:auth]
1267 use = egg:swift#auth
1268 # The reseller prefix will verify a token begins with this prefix before even
1269@@ -44,7 +48,37 @@
1270 # ip = 127.0.0.1
1271 # port = 11000
1272 # ssl = false
1273-# node_timeout = 10
1274+# prefix = /
1275+# node_timeout = 10
1276+
1277+# Only needed for Swauth
1278+[filter:swauth]
1279+use = egg:swift#swauth
1280+# log_name = auth-server
1281+# log_facility = LOG_LOCAL0
1282+# log_level = INFO
1283+# log_headers = False
1284+# The reseller prefix will verify a token begins with this prefix before even
1285+# attempting to validate it. Also, with authorization, only Swift storage
1286+# accounts with this prefix will be authorized by this middleware. Useful if
1287+# multiple auth systems are in use for one Swift cluster.
1288+# reseller_prefix = AUTH
1289+# The auth prefix will cause requests beginning with this prefix to be routed
1290+# to the auth subsystem, for granting tokens, creating accounts, users, etc.
1291+# auth_prefix = /auth/
1292+# Cluster strings are of the format name:url where name is a short name for the
1293+# Swift cluster and url is the url to the proxy server(s) for the cluster.
1294+# default_swift_cluster = local:http://127.0.0.1:8080/v1
1295+# You may also use the format name::url::url where the first url is the one
1296+# given to users to access their account (public url) and the second is the one
1297+# used by swauth itself to create and delete accounts (private url). This is
1298+# useful when a load balancer url should be used by users, but swauth itself is
1299+# behind the load balancer. Example:
1300+# default_swift_cluster = local::https://public.com:8080/v1::http://private.com:8080/v1
1301+# token_life = 86400
1302+# node_timeout = 10
1303+# Highly recommended to change this.
1304+super_admin_key = swauthkey
1305
1306 [filter:healthcheck]
1307 use = egg:swift#healthcheck
1308
1309=== modified file 'etc/stats.conf-sample'
1310--- etc/stats.conf-sample 2010-07-19 16:25:18 +0000
1311+++ etc/stats.conf-sample 2011-01-10 20:41:38 +0000
1312@@ -1,5 +1,8 @@
1313 [stats]
1314+# For DevAuth:
1315 auth_url = http://saio:11000/auth
1316+# For Swauth:
1317+# auth_url = http://saio:8080/auth/v1.0
1318 auth_user = test:tester
1319 auth_key = testing
1320 # swift_dir = /etc/swift
1321
1322=== modified file 'setup.py'
1323--- setup.py 2011-01-04 23:34:43 +0000
1324+++ setup.py 2011-01-10 20:41:38 +0000
1325@@ -21,6 +21,7 @@
1326
1327 from swift import __version__ as version
1328
1329+
1330 class local_sdist(sdist):
1331 """Customized sdist hook - builds the ChangeLog file from VC first"""
1332
1333@@ -79,6 +80,10 @@
1334 'bin/swift-log-uploader',
1335 'bin/swift-log-stats-collector',
1336 'bin/swift-account-stats-logger',
1337+ 'bin/swauth-add-account', 'bin/swauth-add-user',
1338+ 'bin/swauth-cleanup-tokens', 'bin/swauth-delete-account',
1339+ 'bin/swauth-delete-user', 'bin/swauth-list', 'bin/swauth-prep',
1340+ 'bin/swauth-set-account-service', 'bin/swift-auth-to-swauth',
1341 ],
1342 entry_points={
1343 'paste.app_factory': [
1344@@ -90,6 +95,7 @@
1345 ],
1346 'paste.filter_factory': [
1347 'auth=swift.common.middleware.auth:filter_factory',
1348+ 'swauth=swift.common.middleware.swauth:filter_factory',
1349 'healthcheck=swift.common.middleware.healthcheck:filter_factory',
1350 'memcache=swift.common.middleware.memcache:filter_factory',
1351 'ratelimit=swift.common.middleware.ratelimit:filter_factory',
1352
1353=== modified file 'swift/common/middleware/auth.py'
1354--- swift/common/middleware/auth.py 2011-01-04 23:34:43 +0000
1355+++ swift/common/middleware/auth.py 2011-01-10 20:41:38 +0000
1356@@ -35,6 +35,7 @@
1357 self.auth_host = conf.get('ip', '127.0.0.1')
1358 self.auth_port = int(conf.get('port', 11000))
1359 self.ssl = conf.get('ssl', 'false').lower() in TRUE_VALUES
1360+ self.auth_prefix = conf.get('prefix', '/')
1361 self.timeout = int(conf.get('node_timeout', 10))
1362
1363 def __call__(self, env, start_response):
1364@@ -131,7 +132,7 @@
1365 if not groups:
1366 with Timeout(self.timeout):
1367 conn = http_connect(self.auth_host, self.auth_port, 'GET',
1368- '/token/%s' % token, ssl=self.ssl)
1369+ '%stoken/%s' % (self.auth_prefix, token), ssl=self.ssl)
1370 resp = conn.getresponse()
1371 resp.read()
1372 conn.close()
1373@@ -158,9 +159,10 @@
1374 user_groups = (req.remote_user or '').split(',')
1375 if '.reseller_admin' in user_groups:
1376 return None
1377- if account in user_groups and (req.method != 'PUT' or container):
1378+ if account in user_groups and \
1379+ (req.method not in ('DELETE', 'PUT') or container):
1380 # If the user is admin for the account and is not trying to do an
1381- # account PUT...
1382+ # account DELETE or PUT...
1383 return None
1384 referrers, groups = parse_acl(getattr(req, 'acl', None))
1385 if referrer_allowed(req.referer, referrers):
1386
1387=== added file 'swift/common/middleware/swauth.py'
1388--- swift/common/middleware/swauth.py 1970-01-01 00:00:00 +0000
1389+++ swift/common/middleware/swauth.py 2011-01-10 20:41:38 +0000
1390@@ -0,0 +1,1312 @@
1391+# Copyright (c) 2010 OpenStack, LLC.
1392+#
1393+# Licensed under the Apache License, Version 2.0 (the "License");
1394+# you may not use this file except in compliance with the License.
1395+# You may obtain a copy of the License at
1396+#
1397+# http://www.apache.org/licenses/LICENSE-2.0
1398+#
1399+# Unless required by applicable law or agreed to in writing, software
1400+# distributed under the License is distributed on an "AS IS" BASIS,
1401+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
1402+# implied.
1403+# See the License for the specific language governing permissions and
1404+# limitations under the License.
1405+
1406+try:
1407+ import simplejson as json
1408+except ImportError:
1409+ import json
1410+from httplib import HTTPConnection, HTTPSConnection
1411+from time import gmtime, strftime, time
1412+from traceback import format_exc
1413+from urllib import quote, unquote
1414+from urlparse import urlparse
1415+from uuid import uuid4
1416+
1417+from eventlet.timeout import Timeout
1418+from webob import Response, Request
1419+from webob.exc import HTTPAccepted, HTTPBadRequest, HTTPConflict, \
1420+ HTTPCreated, HTTPForbidden, HTTPNoContent, HTTPNotFound, \
1421+ HTTPServiceUnavailable, HTTPUnauthorized
1422+
1423+from swift.common.bufferedhttp import http_connect_raw as http_connect
1424+from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
1425+from swift.common.utils import cache_from_env, get_logger, split_path
1426+
1427+
1428+class Swauth(object):
1429+ """
1430+ Scalable authentication and authorization system that uses Swift as its
1431+ backing store.
1432+
1433+ :param app: The next WSGI app in the pipeline
1434+ :param conf: The dict of configuration values
1435+ """
1436+
1437+ def __init__(self, app, conf):
1438+ self.app = app
1439+ self.conf = conf
1440+ self.logger = get_logger(conf)
1441+ self.log_headers = conf.get('log_headers') == 'True'
1442+ self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip()
1443+ if self.reseller_prefix and self.reseller_prefix[-1] != '_':
1444+ self.reseller_prefix += '_'
1445+ self.auth_prefix = conf.get('auth_prefix', '/auth/')
1446+ if not self.auth_prefix:
1447+ self.auth_prefix = '/auth/'
1448+ if self.auth_prefix[0] != '/':
1449+ self.auth_prefix = '/' + self.auth_prefix
1450+ if self.auth_prefix[-1] != '/':
1451+ self.auth_prefix += '/'
1452+ self.auth_account = '%s.auth' % self.reseller_prefix
1453+ self.default_swift_cluster = conf.get('default_swift_cluster',
1454+ 'local:http://127.0.0.1:8080/v1')
1455+ # This setting is a little messy because of the options it has to
1456+ # provide. The basic format is cluster_name:url, such as the default
1457+ # value of local:http://127.0.0.1:8080/v1. But, often the url given to
1458+ # the user needs to be different than the url used by Swauth to
1459+ # create/delete accounts. So there's a more complex format of
1460+ # cluster_name::url::url, such as
1461+ # local::https://public.com:8080/v1::http://private.com:8080/v1.
1462+ # The double colon is what sets the two apart.
1463+ if '::' in self.default_swift_cluster:
1464+ self.dsc_name, self.dsc_url, self.dsc_url2 = \
1465+ self.default_swift_cluster.split('::', 2)
1466+ self.dsc_url = self.dsc_url.rstrip('/')
1467+ self.dsc_url2 = self.dsc_url2.rstrip('/')
1468+ else:
1469+ self.dsc_name, self.dsc_url = \
1470+ self.default_swift_cluster.split(':', 1)
1471+ self.dsc_url = self.dsc_url2 = self.dsc_url.rstrip('/')
1472+ self.dsc_parsed = urlparse(self.dsc_url)
1473+ if self.dsc_parsed.scheme not in ('http', 'https'):
1474+ raise Exception('Cannot handle protocol scheme %s for url %s' %
1475+ (self.dsc_parsed.scheme, repr(self.dsc_url)))
1476+ self.dsc_parsed2 = urlparse(self.dsc_url2)
1477+ if self.dsc_parsed2.scheme not in ('http', 'https'):
1478+ raise Exception('Cannot handle protocol scheme %s for url %s' %
1479+ (self.dsc_parsed2.scheme, repr(self.dsc_url2)))
1480+ self.super_admin_key = conf.get('super_admin_key')
1481+ if not self.super_admin_key:
1482+ msg = _('No super_admin_key set in conf file! Exiting.')
1483+ try:
1484+ self.logger.critical(msg)
1485+ except Exception:
1486+ pass
1487+ raise ValueError(msg)
1488+ self.token_life = int(conf.get('token_life', 86400))
1489+ self.timeout = int(conf.get('node_timeout', 10))
1490+ self.itoken = None
1491+ self.itoken_expires = None
1492+
1493+ def __call__(self, env, start_response):
1494+ """
1495+ Accepts a standard WSGI application call, authenticating the request
1496+ and installing callback hooks for authorization and ACL header
1497+ validation. For an authenticated request, REMOTE_USER will be set to a
1498+ comma separated list of the user's groups.
1499+
1500+ With a non-empty reseller prefix, acts as the definitive auth service
1501+ for just tokens and accounts that begin with that prefix, but will deny
1502+ requests outside this prefix if no other auth middleware overrides it.
1503+
1504+ With an empty reseller prefix, acts as the definitive auth service only
1505+ for tokens that validate to a non-empty set of groups. For all other
1506+ requests, acts as the fallback auth service when no other auth
1507+ middleware overrides it.
1508+
1509+ Alternatively, if the request matches the self.auth_prefix, the request
1510+ will be routed through the internal auth request handler (self.handle).
1511+ This is to handle creating users, accounts, granting tokens, etc.
1512+ """
1513+ if 'HTTP_X_CF_TRANS_ID' not in env:
1514+ env['HTTP_X_CF_TRANS_ID'] = 'tx' + str(uuid4())
1515+ if env.get('PATH_INFO', '').startswith(self.auth_prefix):
1516+ return self.handle(env, start_response)
1517+ token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN'))
1518+ if token and token.startswith(self.reseller_prefix):
1519+ # Note: Empty reseller_prefix will match all tokens.
1520+ groups = self.get_groups(env, token)
1521+ if groups:
1522+ env['REMOTE_USER'] = groups
1523+ user = groups and groups.split(',', 1)[0] or ''
1524+ # We know the proxy logs the token, so we augment it just a bit
1525+ # to also log the authenticated user.
1526+ env['HTTP_X_AUTH_TOKEN'] = '%s,%s' % (user, token)
1527+ env['swift.authorize'] = self.authorize
1528+ env['swift.clean_acl'] = clean_acl
1529+ else:
1530+ # Unauthorized token
1531+ if self.reseller_prefix:
1532+ # Because I know I'm the definitive auth for this token, I
1533+ # can deny it outright.
1534+ return HTTPUnauthorized()(env, start_response)
1535+ # Because I'm not certain if I'm the definitive auth for empty
1536+ # reseller_prefixed tokens, I won't overwrite swift.authorize.
1537+ elif 'swift.authorize' not in env:
1538+ env['swift.authorize'] = self.denied_response
1539+ else:
1540+ if self.reseller_prefix:
1541+ # With a non-empty reseller_prefix, I would like to be called
1542+ # back for anonymous access to accounts I know I'm the
1543+ # definitive auth for.
1544+ try:
1545+ version, rest = split_path(env.get('PATH_INFO', ''),
1546+ 1, 2, True)
1547+ except ValueError:
1548+ return HTTPNotFound()(env, start_response)
1549+ if rest and rest.startswith(self.reseller_prefix):
1550+ # Handle anonymous access to accounts I'm the definitive
1551+ # auth for.
1552+ env['swift.authorize'] = self.authorize
1553+ env['swift.clean_acl'] = clean_acl
1554+ # Not my token, not my account, I can't authorize this request,
1555+ # deny all is a good idea if not already set...
1556+ elif 'swift.authorize' not in env:
1557+ env['swift.authorize'] = self.denied_response
1558+ # Because I'm not certain if I'm the definitive auth for empty
1559+ # reseller_prefixed accounts, I won't overwrite swift.authorize.
1560+ elif 'swift.authorize' not in env:
1561+ env['swift.authorize'] = self.authorize
1562+ env['swift.clean_acl'] = clean_acl
1563+ return self.app(env, start_response)
1564+
1565+ def get_groups(self, env, token):
1566+ """
1567+ Get groups for the given token.
1568+
1569+ :param env: The current WSGI environment dictionary.
1570+ :param token: Token to validate and return a group string for.
1571+
1572+ :returns: None if the token is invalid or a string containing a comma
1573+ separated list of groups the authenticated user is a member
1574+ of. The first group in the list is also considered a unique
1575+ identifier for that user.
1576+ """
1577+ groups = None
1578+ memcache_client = cache_from_env(env)
1579+ if memcache_client:
1580+ memcache_key = '%s/auth/%s' % (self.reseller_prefix, token)
1581+ cached_auth_data = memcache_client.get(memcache_key)
1582+ if cached_auth_data:
1583+ expires, groups = cached_auth_data
1584+ if expires < time():
1585+ groups = None
1586+ if not groups:
1587+ path = quote('/v1/%s/.token_%s/%s' %
1588+ (self.auth_account, token[-1], token))
1589+ resp = self.make_request(env, 'GET', path).get_response(self.app)
1590+ if resp.status_int // 100 != 2:
1591+ return None
1592+ detail = json.loads(resp.body)
1593+ if detail['expires'] < time():
1594+ self.make_request(env, 'DELETE', path).get_response(self.app)
1595+ return None
1596+ groups = [g['name'] for g in detail['groups']]
1597+ if '.admin' in groups:
1598+ groups.remove('.admin')
1599+ groups.append(detail['account_id'])
1600+ groups = ','.join(groups)
1601+ if memcache_client:
1602+ memcache_client.set(memcache_key, (detail['expires'], groups),
1603+ timeout=float(detail['expires'] - time()))
1604+ return groups
1605+
1606+ def authorize(self, req):
1607+ """
1608+ Returns None if the request is authorized to continue or a standard
1609+ WSGI response callable if not.
1610+ """
1611+ try:
1612+ version, account, container, obj = split_path(req.path, 1, 4, True)
1613+ except ValueError:
1614+ return HTTPNotFound(request=req)
1615+ if not account or not account.startswith(self.reseller_prefix):
1616+ return self.denied_response(req)
1617+ user_groups = (req.remote_user or '').split(',')
1618+ if '.reseller_admin' in user_groups and \
1619+ account != self.reseller_prefix and \
1620+ account[len(self.reseller_prefix)].isalnum():
1621+ return None
1622+ if account in user_groups and \
1623+ (req.method not in ('DELETE', 'PUT') or container):
1624+ # If the user is admin for the account and is not trying to do an
1625+ # account DELETE or PUT...
1626+ return None
1627+ referrers, groups = parse_acl(getattr(req, 'acl', None))
1628+ if referrer_allowed(req.referer, referrers):
1629+ return None
1630+ if not req.remote_user:
1631+ return self.denied_response(req)
1632+ for user_group in user_groups:
1633+ if user_group in groups:
1634+ return None
1635+ return self.denied_response(req)
1636+
1637+ def denied_response(self, req):
1638+ """
1639+ Returns a standard WSGI response callable with the status of 403 or 401
1640+ depending on whether the REMOTE_USER is set or not.
1641+ """
1642+ if req.remote_user:
1643+ return HTTPForbidden(request=req)
1644+ else:
1645+ return HTTPUnauthorized(request=req)
1646+
1647+ def handle(self, env, start_response):
1648+ """
1649+ WSGI entry point for auth requests (ones that match the
1650+ self.auth_prefix).
1651+ Wraps env in webob.Request object and passes it down.
1652+
1653+ :param env: WSGI environment dictionary
1654+ :param start_response: WSGI callable
1655+ """
1656+ try:
1657+ req = Request(env)
1658+ if self.auth_prefix:
1659+ req.path_info_pop()
1660+ req.bytes_transferred = '-'
1661+ req.client_disconnect = False
1662+ if 'x-storage-token' in req.headers and \
1663+ 'x-auth-token' not in req.headers:
1664+ req.headers['x-auth-token'] = req.headers['x-storage-token']
1665+ if 'eventlet.posthooks' in env:
1666+ env['eventlet.posthooks'].append(
1667+ (self.posthooklogger, (req,), {}))
1668+ return self.handle_request(req)(env, start_response)
1669+ else:
1670+ # Lack of posthook support means that we have to log on the
1671+ # start of the response, rather than after all the data has
1672+ # been sent. This prevents logging client disconnects
1673+ # differently than full transmissions.
1674+ response = self.handle_request(req)(env, start_response)
1675+ self.posthooklogger(env, req)
1676+ return response
1677+ except:
1678+ print "EXCEPTION IN handle: %s: %s" % (format_exc(), env)
1679+ start_response('500 Server Error',
1680+ [('Content-Type', 'text/plain')])
1681+ return ['Internal server error.\n']
1682+
1683+ def handle_request(self, req):
1684+ """
1685+ Entry point for auth requests (ones that match the self.auth_prefix).
1686+ Should return a WSGI-style callable (such as webob.Response).
1687+
1688+ :param req: webob.Request object
1689+ """
1690+ req.start_time = time()
1691+ handler = None
1692+ try:
1693+ version, account, user, _ = split_path(req.path_info, minsegs=1,
1694+ maxsegs=4, rest_with_last=True)
1695+ except ValueError:
1696+ return HTTPNotFound(request=req)
1697+ if version in ('v1', 'v1.0', 'auth'):
1698+ if req.method == 'GET':
1699+ handler = self.handle_get_token
1700+ elif version == 'v2':
1701+ req.path_info_pop()
1702+ if req.method == 'GET':
1703+ if not account and not user:
1704+ handler = self.handle_get_reseller
1705+ elif account:
1706+ if not user:
1707+ handler = self.handle_get_account
1708+ elif account == '.token':
1709+ req.path_info_pop()
1710+ handler = self.handle_validate_token
1711+ else:
1712+ handler = self.handle_get_user
1713+ elif req.method == 'PUT':
1714+ if not user:
1715+ handler = self.handle_put_account
1716+ else:
1717+ handler = self.handle_put_user
1718+ elif req.method == 'DELETE':
1719+ if not user:
1720+ handler = self.handle_delete_account
1721+ else:
1722+ handler = self.handle_delete_user
1723+ elif req.method == 'POST':
1724+ if account == '.prep':
1725+ handler = self.handle_prep
1726+ elif user == '.services':
1727+ handler = self.handle_set_services
1728+ if not handler:
1729+ req.response = HTTPBadRequest(request=req)
1730+ else:
1731+ req.response = handler(req)
1732+ return req.response
1733+
1734+ def handle_prep(self, req):
1735+ """
1736+ Handles the POST v2/.prep call for preparing the backing store Swift
1737+ cluster for use with the auth subsystem. Can only be called by
1738+ .super_admin.
1739+
1740+ :param req: The webob.Request to process.
1741+ :returns: webob.Response, 204 on success
1742+ """
1743+ if not self.is_super_admin(req):
1744+ return HTTPForbidden(request=req)
1745+ path = quote('/v1/%s' % self.auth_account)
1746+ resp = self.make_request(req.environ, 'PUT',
1747+ path).get_response(self.app)
1748+ if resp.status_int // 100 != 2:
1749+ raise Exception('Could not create the main auth account: %s %s' %
1750+ (path, resp.status))
1751+ path = quote('/v1/%s/.account_id' % self.auth_account)
1752+ resp = self.make_request(req.environ, 'PUT',
1753+ path).get_response(self.app)
1754+ if resp.status_int // 100 != 2:
1755+ raise Exception('Could not create container: %s %s' %
1756+ (path, resp.status))
1757+ for container in xrange(16):
1758+ path = quote('/v1/%s/.token_%x' % (self.auth_account, container))
1759+ resp = self.make_request(req.environ, 'PUT',
1760+ path).get_response(self.app)
1761+ if resp.status_int // 100 != 2:
1762+ raise Exception('Could not create container: %s %s' %
1763+ (path, resp.status))
1764+ return HTTPNoContent(request=req)
1765+
1766+ def handle_get_reseller(self, req):
1767+ """
1768+ Handles the GET v2 call for getting general reseller information
1769+ (currently just a list of accounts). Can only be called by a
1770+ .reseller_admin.
1771+
1772+ On success, a JSON dictionary will be returned with a single `accounts`
1773+ key whose value is list of dicts. Each dict represents an account and
1774+ currently only contains the single key `name`. For example::
1775+
1776+ {"accounts": [{"name": "reseller"}, {"name": "test"},
1777+ {"name": "test2"}]}
1778+
1779+ :param req: The webob.Request to process.
1780+ :returns: webob.Response, 2xx on success with a JSON dictionary as
1781+ explained above.
1782+ """
1783+ if not self.is_reseller_admin(req):
1784+ return HTTPForbidden(request=req)
1785+ listing = []
1786+ marker = ''
1787+ while True:
1788+ path = '/v1/%s?format=json&marker=%s' % (quote(self.auth_account),
1789+ quote(marker))
1790+ resp = self.make_request(req.environ, 'GET',
1791+ path).get_response(self.app)
1792+ if resp.status_int // 100 != 2:
1793+ raise Exception('Could not list main auth account: %s %s' %
1794+ (path, resp.status))
1795+ sublisting = json.loads(resp.body)
1796+ if not sublisting:
1797+ break
1798+ for container in sublisting:
1799+ if container['name'][0] != '.':
1800+ listing.append({'name': container['name']})
1801+ marker = sublisting[-1]['name']
1802+ return Response(body=json.dumps({'accounts': listing}))
1803+
1804+ def handle_get_account(self, req):
1805+ """
1806+ Handles the GET v2/<account> call for getting account information.
1807+ Can only be called by an account .admin.
1808+
1809+ On success, a JSON dictionary will be returned containing the keys
1810+ `account_id`, `services`, and `users`. The `account_id` is the value
1811+ used when creating service accounts. The `services` value is a dict as
1812+ described in the :func:`handle_get_token` call. The `users` value is a
1813+ list of dicts, each dict representing a user and currently only
1814+ containing the single key `name`. For example::
1815+
1816+ {"account_id": "AUTH_018c3946-23f8-4efb-a8fb-b67aae8e4162",
1817+ "services": {"storage": {"default": "local",
1818+ "local": "http://127.0.0.1:8080/v1/AUTH_018c3946-23f8-4efb-a8fb-b67aae8e4162"}},
1819+ "users": [{"name": "tester"}, {"name": "tester3"}]}
1820+
1821+ :param req: The webob.Request to process.
1822+ :returns: webob.Response, 2xx on success with a JSON dictionary as
1823+ explained above.
1824+ """
1825+ account = req.path_info_pop()
1826+ if req.path_info or not account.isalnum():
1827+ return HTTPBadRequest(request=req)
1828+ if not self.is_account_admin(req, account):
1829+ return HTTPForbidden(request=req)
1830+ path = quote('/v1/%s/%s/.services' % (self.auth_account, account))
1831+ resp = self.make_request(req.environ, 'GET',
1832+ path).get_response(self.app)
1833+ if resp.status_int == 404:
1834+ return HTTPNotFound(request=req)
1835+ if resp.status_int // 100 != 2:
1836+ raise Exception('Could not obtain the .services object: %s %s' %
1837+ (path, resp.status))
1838+ services = json.loads(resp.body)
1839+ listing = []
1840+ marker = ''
1841+ while True:
1842+ path = '/v1/%s?format=json&marker=%s' % (quote('%s/%s' %
1843+ (self.auth_account, account)), quote(marker))
1844+ resp = self.make_request(req.environ, 'GET',
1845+ path).get_response(self.app)
1846+ if resp.status_int == 404:
1847+ return HTTPNotFound(request=req)
1848+ if resp.status_int // 100 != 2:
1849+ raise Exception('Could not list in main auth account: %s %s' %
1850+ (path, resp.status))
1851+ account_id = resp.headers['X-Container-Meta-Account-Id']
1852+ sublisting = json.loads(resp.body)
1853+ if not sublisting:
1854+ break
1855+ for obj in sublisting:
1856+ if obj['name'][0] != '.':
1857+ listing.append({'name': obj['name']})
1858+ marker = sublisting[-1]['name']
1859+ return Response(body=json.dumps({'account_id': account_id,
1860+ 'services': services, 'users': listing}))
1861+
1862+ def handle_set_services(self, req):
1863+ """
1864+ Handles the POST v2/<account>/.services call for setting services
1865+ information. Can only be called by a reseller .admin.
1866+
1867+ In the :func:`handle_get_account` (GET v2/<account>) call, a section of
1868+ the returned JSON dict is `services`. This section looks something like
1869+ this::
1870+
1871+ "services": {"storage": {"default": "local",
1872+ "local": "http://127.0.0.1:8080/v1/AUTH_018c3946-23f8-4efb-a8fb-b67aae8e4162"}}
1873+
1874+ Making use of this section is described in :func:`handle_get_token`.
1875+
1876+ This function allows setting values within this section for the
1877+ <account>, allowing the addition of new service end points or updating
1878+ existing ones.
1879+
1880+ The body of the POST request should contain a JSON dict with the
1881+ following format::
1882+
1883+ {"service_name": {"end_point_name": "end_point_value"}}
1884+
1885+ There can be multiple services and multiple end points in the same
1886+ call.
1887+
1888+ Any new services or end points will be added to the existing set of
1889+ services and end points. Any existing services with the same service
1890+ name will be merged with the new end points. Any existing end points
1891+ with the same end point name will have their values updated.
1892+
1893+ The updated services dictionary will be returned on success.
1894+
1895+ :param req: The webob.Request to process.
1896+ :returns: webob.Response, 2xx on success with the udpated services JSON
1897+ dict as described above
1898+ """
1899+ if not self.is_reseller_admin(req):
1900+ return HTTPForbidden(request=req)
1901+ account = req.path_info_pop()
1902+ if req.path_info != '/.services' or not account.isalnum():
1903+ return HTTPBadRequest(request=req)
1904+ try:
1905+ new_services = json.loads(req.body)
1906+ except ValueError, err:
1907+ return HTTPBadRequest(body=str(err))
1908+ # Get the current services information
1909+ path = quote('/v1/%s/%s/.services' % (self.auth_account, account))
1910+ resp = self.make_request(req.environ, 'GET',
1911+ path).get_response(self.app)
1912+ if resp.status_int == 404:
1913+ return HTTPNotFound(request=req)
1914+ if resp.status_int // 100 != 2:
1915+ raise Exception('Could not obtain services info: %s %s' %
1916+ (path, resp.status))
1917+ services = json.loads(resp.body)
1918+ for new_service, value in new_services.iteritems():
1919+ if new_service in services:
1920+ services[new_service].update(value)
1921+ else:
1922+ services[new_service] = value
1923+ # Save the new services information
1924+ services = json.dumps(services)
1925+ resp = self.make_request(req.environ, 'PUT', path,
1926+ services).get_response(self.app)
1927+ if resp.status_int // 100 != 2:
1928+ raise Exception('Could not save .services object: %s %s' %
1929+ (path, resp.status))
1930+ return Response(request=req, body=services)
1931+
1932+ def handle_put_account(self, req):
1933+ """
1934+ Handles the PUT v2/<account> call for adding an account to the auth
1935+ system. Can only be called by a .reseller_admin.
1936+
1937+ By default, a newly created UUID4 will be used with the reseller prefix
1938+ as the account id used when creating corresponding service accounts.
1939+ However, you can provide an X-Account-Suffix header to replace the
1940+ UUID4 part.
1941+
1942+ :param req: The webob.Request to process.
1943+ :returns: webob.Response, 2xx on success.
1944+ """
1945+ if not self.is_reseller_admin(req):
1946+ return HTTPForbidden(request=req)
1947+ account = req.path_info_pop()
1948+ if req.path_info or not account.isalnum():
1949+ return HTTPBadRequest(request=req)
1950+ # Ensure the container in the main auth account exists (this
1951+ # container represents the new account)
1952+ path = quote('/v1/%s/%s' % (self.auth_account, account))
1953+ resp = self.make_request(req.environ, 'HEAD',
1954+ path).get_response(self.app)
1955+ if resp.status_int == 404:
1956+ resp = self.make_request(req.environ, 'PUT',
1957+ path).get_response(self.app)
1958+ if resp.status_int // 100 != 2:
1959+ raise Exception('Could not create account within main auth '
1960+ 'account: %s %s' % (path, resp.status))
1961+ elif resp.status_int // 100 == 2:
1962+ if 'x-container-meta-account-id' in resp.headers:
1963+ # Account was already created
1964+ return HTTPAccepted(request=req)
1965+ else:
1966+ raise Exception('Could not verify account within main auth '
1967+ 'account: %s %s' % (path, resp.status))
1968+ account_suffix = req.headers.get('x-account-suffix')
1969+ if not account_suffix:
1970+ account_suffix = str(uuid4())
1971+ # Create the new account in the Swift cluster
1972+ path = quote('%s/%s%s' % (self.dsc_parsed2.path,
1973+ self.reseller_prefix, account_suffix))
1974+ try:
1975+ conn = self.get_conn()
1976+ conn.request('PUT', path,
1977+ headers={'X-Auth-Token': self.get_itoken(req.environ)})
1978+ resp = conn.getresponse()
1979+ resp.read()
1980+ if resp.status // 100 != 2:
1981+ raise Exception('Could not create account on the Swift '
1982+ 'cluster: %s %s %s' % (path, resp.status, resp.reason))
1983+ except:
1984+ self.logger.error(_('ERROR: Exception while trying to communicate '
1985+ 'with %(scheme)s://%(host)s:%(port)s/%(path)s'),
1986+ {'scheme': self.dsc_parsed2.scheme,
1987+ 'host': self.dsc_parsed2.hostname,
1988+ 'port': self.dsc_parsed2.port, 'path': path})
1989+ raise
1990+ # Record the mapping from account id back to account name
1991+ path = quote('/v1/%s/.account_id/%s%s' %
1992+ (self.auth_account, self.reseller_prefix, account_suffix))
1993+ resp = self.make_request(req.environ, 'PUT', path,
1994+ account).get_response(self.app)
1995+ if resp.status_int // 100 != 2:
1996+ raise Exception('Could not create account id mapping: %s %s' %
1997+ (path, resp.status))
1998+ # Record the cluster url(s) for the account
1999+ path = quote('/v1/%s/%s/.services' % (self.auth_account, account))
2000+ services = {'storage': {}}
2001+ services['storage'][self.dsc_name] = '%s/%s%s' % (self.dsc_url,
2002+ self.reseller_prefix, account_suffix)
2003+ services['storage']['default'] = self.dsc_name
2004+ resp = self.make_request(req.environ, 'PUT', path,
2005+ json.dumps(services)).get_response(self.app)
2006+ if resp.status_int // 100 != 2:
2007+ raise Exception('Could not create .services object: %s %s' %
2008+ (path, resp.status))
2009+ # Record the mapping from account name to the account id
2010+ path = quote('/v1/%s/%s' % (self.auth_account, account))
2011+ resp = self.make_request(req.environ, 'POST', path,
2012+ headers={'X-Container-Meta-Account-Id': '%s%s' %
2013+ (self.reseller_prefix, account_suffix)}).get_response(self.app)
2014+ if resp.status_int // 100 != 2:
2015+ raise Exception('Could not record the account id on the account: '
2016+ '%s %s' % (path, resp.status))
2017+ return HTTPCreated(request=req)
2018+
2019+ def handle_delete_account(self, req):
2020+ """
2021+ Handles the DELETE v2/<account> call for removing an account from the
2022+ auth system. Can only be called by a .reseller_admin.
2023+
2024+ :param req: The webob.Request to process.
2025+ :returns: webob.Response, 2xx on success.
2026+ """
2027+ if not self.is_reseller_admin(req):
2028+ return HTTPForbidden(request=req)
2029+ account = req.path_info_pop()
2030+ if req.path_info or not account.isalnum():
2031+ return HTTPBadRequest(request=req)
2032+ # Make sure the account has no users and get the account_id
2033+ marker = ''
2034+ while True:
2035+ path = '/v1/%s?format=json&marker=%s' % (quote('%s/%s' %
2036+ (self.auth_account, account)), quote(marker))
2037+ resp = self.make_request(req.environ, 'GET',
2038+ path).get_response(self.app)
2039+ if resp.status_int == 404:
2040+ return HTTPNotFound(request=req)
2041+ if resp.status_int // 100 != 2:
2042+ raise Exception('Could not list in main auth account: %s %s' %
2043+ (path, resp.status))
2044+ account_id = resp.headers['x-container-meta-account-id']
2045+ sublisting = json.loads(resp.body)
2046+ if not sublisting:
2047+ break
2048+ for obj in sublisting:
2049+ if obj['name'][0] != '.':
2050+ return HTTPConflict(request=req)
2051+ marker = sublisting[-1]['name']
2052+ # Obtain the listing of services the account is on.
2053+ path = quote('/v1/%s/%s/.services' % (self.auth_account, account))
2054+ resp = self.make_request(req.environ, 'GET',
2055+ path).get_response(self.app)
2056+ if resp.status_int // 100 != 2 and resp.status_int != 404:
2057+ raise Exception('Could not obtain .services object: %s %s' %
2058+ (path, resp.status))
2059+ if resp.status_int // 100 == 2:
2060+ services = json.loads(resp.body)
2061+ # Delete the account on each cluster it is on.
2062+ deleted_any = False
2063+ for name, url in services['storage'].iteritems():
2064+ if name != 'default':
2065+ parsed = urlparse(url)
2066+ conn = self.get_conn(parsed)
2067+ conn.request('DELETE', parsed.path,
2068+ headers={'X-Auth-Token': self.get_itoken(req.environ)})
2069+ resp = conn.getresponse()
2070+ resp.read()
2071+ if resp.status == 409:
2072+ if deleted_any:
2073+ raise Exception('Managed to delete one or more '
2074+ 'service end points, but failed with: '
2075+ '%s %s %s' % (url, resp.status, resp.reason))
2076+ else:
2077+ return HTTPConflict(request=req)
2078+ if resp.status // 100 != 2 and resp.status != 404:
2079+ raise Exception('Could not delete account on the '
2080+ 'Swift cluster: %s %s %s' %
2081+ (url, resp.status, resp.reason))
2082+ deleted_any = True
2083+ # Delete the .services object itself.
2084+ path = quote('/v1/%s/%s/.services' %
2085+ (self.auth_account, account))
2086+ resp = self.make_request(req.environ, 'DELETE',
2087+ path).get_response(self.app)
2088+ if resp.status_int // 100 != 2 and resp.status_int != 404:
2089+ raise Exception('Could not delete .services object: %s %s' %
2090+ (path, resp.status))
2091+ # Delete the account id mapping for the account.
2092+ path = quote('/v1/%s/.account_id/%s' %
2093+ (self.auth_account, account_id))
2094+ resp = self.make_request(req.environ, 'DELETE',
2095+ path).get_response(self.app)
2096+ if resp.status_int // 100 != 2 and resp.status_int != 404:
2097+ raise Exception('Could not delete account id mapping: %s %s' %
2098+ (path, resp.status))
2099+ # Delete the account marker itself.
2100+ path = quote('/v1/%s/%s' % (self.auth_account, account))
2101+ resp = self.make_request(req.environ, 'DELETE',
2102+ path).get_response(self.app)
2103+ if resp.status_int // 100 != 2 and resp.status_int != 404:
2104+ raise Exception('Could not delete account marked: %s %s' %
2105+ (path, resp.status))
2106+ return HTTPNoContent(request=req)
2107+
2108+ def handle_get_user(self, req):
2109+ """
2110+ Handles the GET v2/<account>/<user> call for getting user information.
2111+ Can only be called by an account .admin.
2112+
2113+ On success, a JSON dict will be returned as described::
2114+
2115+ {"groups": [ # List of groups the user is a member of
2116+ {"name": "<act>:<usr>"},
2117+ # The first group is a unique user identifier
2118+ {"name": "<account>"},
2119+ # The second group is the auth account name
2120+ {"name": "<additional-group>"}
2121+ # There may be additional groups, .admin being a special
2122+ # group indicating an account admin and .reseller_admin
2123+ # indicating a reseller admin.
2124+ ],
2125+ "auth": "plaintext:<key>"
2126+ # The auth-type and key for the user; currently only plaintext is
2127+ # implemented.
2128+ }
2129+
2130+ For example::
2131+
2132+ {"groups": [{"name": "test:tester"}, {"name": "test"},
2133+ {"name": ".admin"}],
2134+ "auth": "plaintext:testing"}
2135+
2136+ If the <user> in the request is the special user `.groups`, the JSON
2137+ dict will contain a single key of `groups` whose value is a list of
2138+ dicts representing the active groups within the account. Each dict
2139+ currently has the single key `name`. For example::
2140+
2141+ {"groups": [{"name": ".admin"}, {"name": "test"},
2142+ {"name": "test:tester"}, {"name": "test:tester3"}]}
2143+
2144+ :param req: The webob.Request to process.
2145+ :returns: webob.Response, 2xx on success with a JSON dictionary as
2146+ explained above.
2147+ """
2148+ account = req.path_info_pop()
2149+ user = req.path_info_pop()
2150+ if req.path_info or not account.isalnum() or \
2151+ (not user.isalnum() and user != '.groups'):
2152+ return HTTPBadRequest(request=req)
2153+ if not self.is_account_admin(req, account):
2154+ return HTTPForbidden(request=req)
2155+ if user == '.groups':
2156+ # TODO: This could be very slow for accounts with a really large
2157+ # number of users. Speed could be improved by concurrently
2158+ # requesting user group information. Then again, I don't *know*
2159+ # it's slow for `normal` use cases, so testing should be done.
2160+ groups = set()
2161+ marker = ''
2162+ while True:
2163+ path = '/v1/%s?format=json&marker=%s' % (quote('%s/%s' %
2164+ (self.auth_account, account)), quote(marker))
2165+ resp = self.make_request(req.environ, 'GET',
2166+ path).get_response(self.app)
2167+ if resp.status_int == 404:
2168+ return HTTPNotFound(request=req)
2169+ if resp.status_int // 100 != 2:
2170+ raise Exception('Could not list in main auth account: '
2171+ '%s %s' % (path, resp.status))
2172+ sublisting = json.loads(resp.body)
2173+ if not sublisting:
2174+ break
2175+ for obj in sublisting:
2176+ if obj['name'][0] != '.':
2177+ path = quote('/v1/%s/%s/%s' % (self.auth_account,
2178+ account, obj['name']))
2179+ resp = self.make_request(req.environ, 'GET',
2180+ path).get_response(self.app)
2181+ if resp.status_int // 100 != 2:
2182+ raise Exception('Could not retrieve user object: '
2183+ '%s %s' % (path, resp.status))
2184+ groups.update(g['name']
2185+ for g in json.loads(resp.body)['groups'])
2186+ marker = sublisting[-1]['name']
2187+ body = json.dumps({'groups':
2188+ [{'name': g} for g in sorted(groups)]})
2189+ else:
2190+ path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user))
2191+ resp = self.make_request(req.environ, 'GET',
2192+ path).get_response(self.app)
2193+ if resp.status_int == 404:
2194+ return HTTPNotFound(request=req)
2195+ if resp.status_int // 100 != 2:
2196+ raise Exception('Could not retrieve user object: %s %s' %
2197+ (path, resp.status))
2198+ body = resp.body
2199+ return Response(body=body)
2200+
2201+ def handle_put_user(self, req):
2202+ """
2203+ Handles the PUT v2/<account>/<user> call for adding a user to an
2204+ account.
2205+
2206+ X-Auth-User-Key represents the user's key, X-Auth-User-Admin may be set
2207+ to `true` to create an account .admin, and X-Auth-User-Reseller-Admin
2208+ may be set to `true` to create a .reseller_admin.
2209+
2210+ Can only be called by an account .admin unless the user is to be a
2211+ .reseller_admin, in which case the request must be by .super_admin.
2212+
2213+ :param req: The webob.Request to process.
2214+ :returns: webob.Response, 2xx on success.
2215+ """
2216+ # Validate path info
2217+ account = req.path_info_pop()
2218+ user = req.path_info_pop()
2219+ key = req.headers.get('x-auth-user-key')
2220+ admin = req.headers.get('x-auth-user-admin') == 'true'
2221+ reseller_admin = \
2222+ req.headers.get('x-auth-user-reseller-admin') == 'true'
2223+ if reseller_admin:
2224+ admin = True
2225+ if req.path_info or not account.isalnum() or not user.isalnum() or \
2226+ not key:
2227+ return HTTPBadRequest(request=req)
2228+ if reseller_admin:
2229+ if not self.is_super_admin(req):
2230+ return HTTPForbidden(request=req)
2231+ elif not self.is_account_admin(req, account):
2232+ return HTTPForbidden(request=req)
2233+ # Create the object in the main auth account (this object represents
2234+ # the user)
2235+ path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user))
2236+ groups = ['%s:%s' % (account, user), account]
2237+ if admin:
2238+ groups.append('.admin')
2239+ if reseller_admin:
2240+ groups.append('.reseller_admin')
2241+ resp = self.make_request(req.environ, 'PUT', path, json.dumps({'auth':
2242+ 'plaintext:%s' % key,
2243+ 'groups': [{'name': g} for g in groups]})).get_response(self.app)
2244+ if resp.status_int == 404:
2245+ return HTTPNotFound(request=req)
2246+ if resp.status_int // 100 != 2:
2247+ raise Exception('Could not create user object: %s %s' %
2248+ (path, resp.status))
2249+ return HTTPCreated(request=req)
2250+
2251+ def handle_delete_user(self, req):
2252+ """
2253+ Handles the DELETE v2/<account>/<user> call for deleting a user from an
2254+ account.
2255+
2256+ Can only be called by an account .admin.
2257+
2258+ :param req: The webob.Request to process.
2259+ :returns: webob.Response, 2xx on success.
2260+ """
2261+ # Validate path info
2262+ account = req.path_info_pop()
2263+ user = req.path_info_pop()
2264+ if req.path_info or not account.isalnum() or not user.isalnum():
2265+ return HTTPBadRequest(request=req)
2266+ if not self.is_account_admin(req, account):
2267+ return HTTPForbidden(request=req)
2268+ # Delete the user's existing token, if any.
2269+ path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user))
2270+ resp = self.make_request(req.environ, 'HEAD',
2271+ path).get_response(self.app)
2272+ if resp.status_int == 404:
2273+ return HTTPNotFound(request=req)
2274+ elif resp.status_int // 100 != 2:
2275+ raise Exception('Could not obtain user details: %s %s' %
2276+ (path, resp.status))
2277+ candidate_token = resp.headers.get('x-object-meta-auth-token')
2278+ if candidate_token:
2279+ path = quote('/v1/%s/.token_%s/%s' %
2280+ (self.auth_account, candidate_token[-1], candidate_token))
2281+ resp = self.make_request(req.environ, 'DELETE',
2282+ path).get_response(self.app)
2283+ if resp.status_int // 100 != 2 and resp.status_int != 404:
2284+ raise Exception('Could not delete possibly existing token: '
2285+ '%s %s' % (path, resp.status))
2286+ # Delete the user entry itself.
2287+ path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user))
2288+ resp = self.make_request(req.environ, 'DELETE',
2289+ path).get_response(self.app)
2290+ if resp.status_int // 100 != 2 and resp.status_int != 404:
2291+ raise Exception('Could not delete the user object: %s %s' %
2292+ (path, resp.status))
2293+ return HTTPNoContent(request=req)
2294+
2295+ def handle_get_token(self, req):
2296+ """
2297+ Handles the various `request for token and service end point(s)` calls.
2298+ There are various formats to support the various auth servers in the
2299+ past. Examples::
2300+
2301+ GET <auth-prefix>/v1/<act>/auth
2302+ X-Auth-User: <act>:<usr> or X-Storage-User: <usr>
2303+ X-Auth-Key: <key> or X-Storage-Pass: <key>
2304+ GET <auth-prefix>/auth
2305+ X-Auth-User: <act>:<usr> or X-Storage-User: <act>:<usr>
2306+ X-Auth-Key: <key> or X-Storage-Pass: <key>
2307+ GET <auth-prefix>/v1.0
2308+ X-Auth-User: <act>:<usr> or X-Storage-User: <act>:<usr>
2309+ X-Auth-Key: <key> or X-Storage-Pass: <key>
2310+
2311+ On successful authentication, the response will have X-Auth-Token and
2312+ X-Storage-Token set to the token to use with Swift and X-Storage-URL
2313+ set to the URL to the default Swift cluster to use.
2314+
2315+ The response body will be set to the account's services JSON object as
2316+ described here::
2317+
2318+ {"storage": { # Represents the Swift storage service end points
2319+ "default": "cluster1", # Indicates which cluster is the default
2320+ "cluster1": "<URL to use with Swift>",
2321+ # A Swift cluster that can be used with this account,
2322+ # "cluster1" is the name of the cluster which is usually a
2323+ # location indicator (like "dfw" for a datacenter region).
2324+ "cluster2": "<URL to use with Swift>"
2325+ # Another Swift cluster that can be used with this account,
2326+ # there will always be at least one Swift cluster to use or
2327+ # this whole "storage" dict won't be included at all.
2328+ },
2329+ "servers": { # Represents the Nova server service end points
2330+ # Expected to be similar to the "storage" dict, but not
2331+ # implemented yet.
2332+ },
2333+ # Possibly other service dicts, not implemented yet.
2334+ }
2335+
2336+ :param req: The webob.Request to process.
2337+ :returns: webob.Response, 2xx on success with data set as explained
2338+ above.
2339+ """
2340+ # Validate the request info
2341+ try:
2342+ pathsegs = split_path(req.path_info, minsegs=1, maxsegs=3,
2343+ rest_with_last=True)
2344+ except ValueError:
2345+ return HTTPNotFound(request=req)
2346+ if pathsegs[0] == 'v1' and pathsegs[2] == 'auth':
2347+ account = pathsegs[1]
2348+ user = req.headers.get('x-storage-user')
2349+ if not user:
2350+ user = req.headers.get('x-auth-user')
2351+ if not user or ':' not in user:
2352+ return HTTPUnauthorized(request=req)
2353+ account2, user = user.split(':', 1)
2354+ if account != account2:
2355+ return HTTPUnauthorized(request=req)
2356+ key = req.headers.get('x-storage-pass')
2357+ if not key:
2358+ key = req.headers.get('x-auth-key')
2359+ elif pathsegs[0] in ('auth', 'v1.0'):
2360+ user = req.headers.get('x-auth-user')
2361+ if not user:
2362+ user = req.headers.get('x-storage-user')
2363+ if not user or ':' not in user:
2364+ return HTTPUnauthorized(request=req)
2365+ account, user = user.split(':', 1)
2366+ key = req.headers.get('x-auth-key')
2367+ if not key:
2368+ key = req.headers.get('x-storage-pass')
2369+ else:
2370+ return HTTPBadRequest(request=req)
2371+ if not all((account, user, key)):
2372+ return HTTPUnauthorized(request=req)
2373+ if user == '.super_admin' and key == self.super_admin_key:
2374+ token = self.get_itoken(req.environ)
2375+ url = '%s/%s.auth' % (self.dsc_url, self.reseller_prefix)
2376+ return Response(request=req,
2377+ body=json.dumps({'storage': {'default': 'local', 'local': url}}),
2378+ headers={'x-auth-token': token, 'x-storage-token': token,
2379+ 'x-storage-url': url})
2380+ # Authenticate user
2381+ path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user))
2382+ resp = self.make_request(req.environ, 'GET',
2383+ path).get_response(self.app)
2384+ if resp.status_int == 404:
2385+ return HTTPUnauthorized(request=req)
2386+ if resp.status_int // 100 != 2:
2387+ raise Exception('Could not obtain user details: %s %s' %
2388+ (path, resp.status))
2389+ user_detail = json.loads(resp.body)
2390+ if not self.credentials_match(user_detail, key):
2391+ return HTTPUnauthorized(request=req)
2392+ # See if a token already exists and hasn't expired
2393+ token = None
2394+ candidate_token = resp.headers.get('x-object-meta-auth-token')
2395+ if candidate_token:
2396+ path = quote('/v1/%s/.token_%s/%s' %
2397+ (self.auth_account, candidate_token[-1], candidate_token))
2398+ resp = self.make_request(req.environ, 'GET',
2399+ path).get_response(self.app)
2400+ if resp.status_int // 100 == 2:
2401+ token_detail = json.loads(resp.body)
2402+ if token_detail['expires'] > time():
2403+ token = candidate_token
2404+ else:
2405+ self.make_request(req.environ, 'DELETE',
2406+ path).get_response(self.app)
2407+ elif resp.status_int != 404:
2408+ raise Exception('Could not detect whether a token already '
2409+ 'exists: %s %s' % (path, resp.status))
2410+ # Create a new token if one didn't exist
2411+ if not token:
2412+ # Retrieve account id, we'll save this in the token
2413+ path = quote('/v1/%s/%s' % (self.auth_account, account))
2414+ resp = self.make_request(req.environ, 'HEAD',
2415+ path).get_response(self.app)
2416+ if resp.status_int // 100 != 2:
2417+ raise Exception('Could not retrieve account id value: '
2418+ '%s %s' % (path, resp.status))
2419+ account_id = \
2420+ resp.headers['x-container-meta-account-id']
2421+ # Generate new token
2422+ token = '%stk%s' % (self.reseller_prefix, uuid4().hex)
2423+ # Save token info
2424+ path = quote('/v1/%s/.token_%s/%s' %
2425+ (self.auth_account, token[-1], token))
2426+ resp = self.make_request(req.environ, 'PUT', path,
2427+ json.dumps({'account': account, 'user': user,
2428+ 'account_id': account_id,
2429+ 'groups': user_detail['groups'],
2430+ 'expires': time() + self.token_life})).get_response(self.app)
2431+ if resp.status_int // 100 != 2:
2432+ raise Exception('Could not create new token: %s %s' %
2433+ (path, resp.status))
2434+ # Record the token with the user info for future use.
2435+ path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user))
2436+ resp = self.make_request(req.environ, 'POST', path,
2437+ headers={'X-Object-Meta-Auth-Token': token}
2438+ ).get_response(self.app)
2439+ if resp.status_int // 100 != 2:
2440+ raise Exception('Could not save new token: %s %s' %
2441+ (path, resp.status))
2442+ # Get the services information
2443+ path = quote('/v1/%s/%s/.services' % (self.auth_account, account))
2444+ resp = self.make_request(req.environ, 'GET',
2445+ path).get_response(self.app)
2446+ if resp.status_int // 100 != 2:
2447+ raise Exception('Could not obtain services info: %s %s' %
2448+ (path, resp.status))
2449+ detail = json.loads(resp.body)
2450+ url = detail['storage'][detail['storage']['default']]
2451+ return Response(request=req, body=resp.body,
2452+ headers={'x-auth-token': token, 'x-storage-token': token,
2453+ 'x-storage-url': url})
2454+
2455+ def handle_validate_token(self, req):
2456+ """
2457+ Handles the GET v2/.token/<token> call for validating a token, usually
2458+ called by a service like Swift.
2459+
2460+ On a successful validation, X-Auth-TTL will be set for how much longer
2461+ this token is valid and X-Auth-Groups will contain a comma separated
2462+ list of groups the user belongs to.
2463+
2464+ The first group listed will be a unique identifier for the user the
2465+ token represents.
2466+
2467+ .reseller_admin is a special group that indicates the user should be
2468+ allowed to do anything on any account.
2469+
2470+ :param req: The webob.Request to process.
2471+ :returns: webob.Response, 2xx on success with data set as explained
2472+ above.
2473+ """
2474+ token = req.path_info_pop()
2475+ if req.path_info or not token.startswith(self.reseller_prefix):
2476+ return HTTPBadRequest(request=req)
2477+ expires = groups = None
2478+ memcache_client = cache_from_env(req.environ)
2479+ if memcache_client:
2480+ memcache_key = '%s/auth/%s' % (self.reseller_prefix, token)
2481+ cached_auth_data = memcache_client.get(memcache_key)
2482+ if cached_auth_data:
2483+ expires, groups = cached_auth_data
2484+ if expires < time():
2485+ groups = None
2486+ if not groups:
2487+ path = quote('/v1/%s/.token_%s/%s' %
2488+ (self.auth_account, token[-1], token))
2489+ resp = self.make_request(req.environ, 'GET',
2490+ path).get_response(self.app)
2491+ if resp.status_int // 100 != 2:
2492+ return HTTPNotFound(request=req)
2493+ detail = json.loads(resp.body)
2494+ expires = detail['expires']
2495+ if expires < time():
2496+ self.make_request(req.environ, 'DELETE',
2497+ path).get_response(self.app)
2498+ return HTTPNotFound(request=req)
2499+ groups = [g['name'] for g in detail['groups']]
2500+ if '.admin' in groups:
2501+ groups.remove('.admin')
2502+ groups.append(detail['account_id'])
2503+ groups = ','.join(groups)
2504+ return HTTPNoContent(headers={'X-Auth-TTL': expires - time(),
2505+ 'X-Auth-Groups': groups})
2506+
2507+ def make_request(self, env, method, path, body=None, headers=None):
2508+ """
2509+ Makes a new webob.Request based on the current env but with the
2510+ parameters specified.
2511+
2512+ :param env: Current WSGI environment dictionary
2513+ :param method: HTTP method of new request
2514+ :param path: HTTP path of new request
2515+ :param body: HTTP body of new request; None by default
2516+ :param headers: Extra HTTP headers of new request; None by default
2517+
2518+ :returns: webob.Request object
2519+ """
2520+ newenv = {'REQUEST_METHOD': method}
2521+ for name in ('swift.cache', 'HTTP_X_CF_TRANS_ID'):
2522+ if name in env:
2523+ newenv[name] = env[name]
2524+ if not headers:
2525+ headers = {}
2526+ if body:
2527+ return Request.blank(path, environ=newenv, body=body,
2528+ headers=headers)
2529+ else:
2530+ return Request.blank(path, environ=newenv, headers=headers)
2531+
2532+ def get_conn(self, urlparsed=None):
2533+ """
2534+ Returns an HTTPConnection based on the urlparse result given or the
2535+ default Swift cluster (internal url) urlparse result.
2536+
2537+ :param urlparsed: The result from urlparse.urlparse or None to use the
2538+ default Swift cluster's value
2539+ """
2540+ if not urlparsed:
2541+ urlparsed = self.dsc_parsed2
2542+ if urlparsed.scheme == 'http':
2543+ return HTTPConnection(urlparsed.netloc)
2544+ else:
2545+ return HTTPSConnection(urlparsed.netloc)
2546+
2547+ def get_itoken(self, env):
2548+ """
2549+ Returns the current internal token to use for the auth system's own
2550+ actions with other services. Each process will create its own
2551+ itoken and the token will be deleted and recreated based on the
2552+ token_life configuration value. The itoken information is stored in
2553+ memcache because the auth process that is asked by Swift to validate
2554+ the token may not be the same as the auth process that created the
2555+ token.
2556+ """
2557+ if not self.itoken or self.itoken_expires < time():
2558+ self.itoken = '%sitk%s' % (self.reseller_prefix, uuid4().hex)
2559+ memcache_key = '%s/auth/%s' % (self.reseller_prefix, self.itoken)
2560+ self.itoken_expires = time() + self.token_life - 60
2561+ memcache_client = cache_from_env(env)
2562+ if not memcache_client:
2563+ raise Exception(
2564+ 'No memcache set up; required for Swauth middleware')
2565+ memcache_client.set(memcache_key, (self.itoken_expires,
2566+ '.auth,.reseller_admin,%s.auth' % self.reseller_prefix),
2567+ timeout=self.token_life)
2568+ return self.itoken
2569+
2570+ def get_admin_detail(self, req):
2571+ """
2572+ Returns the dict for the user specified as the admin in the request
2573+ with the addition of an `account` key set to the admin user's account.
2574+
2575+ :param req: The webob request to retrieve X-Auth-Admin-User and
2576+ X-Auth-Admin-Key from.
2577+ :returns: The dict for the admin user with the addition of the
2578+ `account` key.
2579+ """
2580+ if ':' not in req.headers.get('x-auth-admin-user', ''):
2581+ return None
2582+ admin_account, admin_user = \
2583+ req.headers.get('x-auth-admin-user').split(':', 1)
2584+ path = quote('/v1/%s/%s/%s' % (self.auth_account, admin_account,
2585+ admin_user))
2586+ resp = self.make_request(req.environ, 'GET',
2587+ path).get_response(self.app)
2588+ if resp.status_int == 404:
2589+ return None
2590+ if resp.status_int // 100 != 2:
2591+ raise Exception('Could not get admin user object: %s %s' %
2592+ (path, resp.status))
2593+ admin_detail = json.loads(resp.body)
2594+ admin_detail['account'] = admin_account
2595+ return admin_detail
2596+
2597+ def credentials_match(self, user_detail, key):
2598+ """
2599+ Returns True if the key is valid for the user_detail. Currently, this
2600+ only supports plaintext key matching.
2601+
2602+ :param user_detail: The dict for the user.
2603+ :param key: The key to validate for the user.
2604+ :returns: True if the key is valid for the user, False if not.
2605+ """
2606+ return user_detail and user_detail.get('auth') == 'plaintext:%s' % key
2607+
2608+ def is_super_admin(self, req):
2609+ """
2610+ Returns True if the admin specified in the request represents the
2611+ .super_admin.
2612+
2613+ :param req: The webob.Request to check.
2614+ :param returns: True if .super_admin.
2615+ """
2616+ return req.headers.get('x-auth-admin-user') == '.super_admin' and \
2617+ req.headers.get('x-auth-admin-key') == self.super_admin_key
2618+
2619+ def is_reseller_admin(self, req, admin_detail=None):
2620+ """
2621+ Returns True if the admin specified in the request represents a
2622+ .reseller_admin.
2623+
2624+ :param req: The webob.Request to check.
2625+ :param admin_detail: The previously retrieved dict from
2626+ :func:`get_admin_detail` or None for this function
2627+ to retrieve the admin_detail itself.
2628+ :param returns: True if .reseller_admin.
2629+ """
2630+ if self.is_super_admin(req):
2631+ return True
2632+ if not admin_detail:
2633+ admin_detail = self.get_admin_detail(req)
2634+ if not self.credentials_match(admin_detail,
2635+ req.headers.get('x-auth-admin-key')):
2636+ return False
2637+ return '.reseller_admin' in (g['name'] for g in admin_detail['groups'])
2638+
2639+ def is_account_admin(self, req, account):
2640+ """
2641+ Returns True if the admin specified in the request represents a .admin
2642+ for the account specified.
2643+
2644+ :param req: The webob.Request to check.
2645+ :param account: The account to check for .admin against.
2646+ :param returns: True if .admin.
2647+ """
2648+ if self.is_super_admin(req):
2649+ return True
2650+ admin_detail = self.get_admin_detail(req)
2651+ if admin_detail:
2652+ if self.is_reseller_admin(req, admin_detail=admin_detail):
2653+ return True
2654+ if not self.credentials_match(admin_detail,
2655+ req.headers.get('x-auth-admin-key')):
2656+ return False
2657+ return admin_detail and admin_detail['account'] == account and \
2658+ '.admin' in (g['name'] for g in admin_detail['groups'])
2659+ return False
2660+
2661+ def posthooklogger(self, env, req):
2662+ response = getattr(req, 'response', None)
2663+ if not response:
2664+ return
2665+ trans_time = '%.4f' % (time() - req.start_time)
2666+ the_request = quote(unquote(req.path))
2667+ if req.query_string:
2668+ the_request = the_request + '?' + req.query_string
2669+ # remote user for zeus
2670+ client = req.headers.get('x-cluster-client-ip')
2671+ if not client and 'x-forwarded-for' in req.headers:
2672+ # remote user for other lbs
2673+ client = req.headers['x-forwarded-for'].split(',')[0].strip()
2674+ logged_headers = None
2675+ if self.log_headers:
2676+ logged_headers = '\n'.join('%s: %s' % (k, v)
2677+ for k, v in req.headers.items())
2678+ status_int = response.status_int
2679+ if getattr(req, 'client_disconnect', False) or \
2680+ getattr(response, 'client_disconnect', False):
2681+ status_int = 499
2682+ self.logger.info(' '.join(quote(str(x)) for x in (client or '-',
2683+ req.remote_addr or '-', strftime('%d/%b/%Y/%H/%M/%S', gmtime()),
2684+ req.method, the_request, req.environ['SERVER_PROTOCOL'],
2685+ status_int, req.referer or '-', req.user_agent or '-',
2686+ req.headers.get('x-auth-token',
2687+ req.headers.get('x-auth-admin-user', '-')),
2688+ getattr(req, 'bytes_transferred', 0) or '-',
2689+ getattr(response, 'bytes_transferred', 0) or '-',
2690+ req.headers.get('etag', '-'),
2691+ req.headers.get('x-cf-trans-id', '-'), logged_headers or '-',
2692+ trans_time)))
2693+
2694+
2695+def filter_factory(global_conf, **local_conf):
2696+ """Returns a WSGI filter app for use with paste.deploy."""
2697+ conf = global_conf.copy()
2698+ conf.update(local_conf)
2699+
2700+ def auth_filter(app):
2701+ return Swauth(app, conf)
2702+ return auth_filter
2703
2704=== modified file 'swift/common/utils.py'
2705--- swift/common/utils.py 2011-01-07 21:17:29 +0000
2706+++ swift/common/utils.py 2011-01-10 20:41:38 +0000
2707@@ -396,6 +396,7 @@
2708 root_logger = logging.getLogger()
2709 if hasattr(get_logger, 'handler') and get_logger.handler:
2710 root_logger.removeHandler(get_logger.handler)
2711+ get_logger.handler.close()
2712 get_logger.handler = None
2713 if log_to_console:
2714 # check if a previous call to get_logger already added a console logger
2715
2716=== modified file 'swift/proxy/server.py'
2717--- swift/proxy/server.py 2011-01-07 21:17:29 +0000
2718+++ swift/proxy/server.py 2011-01-10 20:41:38 +0000
2719@@ -1683,7 +1683,8 @@
2720 def update_request(self, req):
2721 req.bytes_transferred = '-'
2722 req.client_disconnect = False
2723- req.headers['x-cf-trans-id'] = 'tx' + str(uuid.uuid4())
2724+ if 'x-cf-trans-id' not in req.headers:
2725+ req.headers['x-cf-trans-id'] = 'tx' + str(uuid.uuid4())
2726 if 'x-storage-token' in req.headers and \
2727 'x-auth-token' not in req.headers:
2728 req.headers['x-auth-token'] = req.headers['x-storage-token']
2729
2730=== modified file 'test/functional/sample.conf'
2731--- test/functional/sample.conf 2010-09-09 17:24:25 +0000
2732+++ test/functional/sample.conf 2011-01-10 20:41:38 +0000
2733@@ -1,7 +1,12 @@
2734 # sample config
2735 auth_host = 127.0.0.1
2736+# For DevAuth:
2737 auth_port = 11000
2738+# For Swauth:
2739+# auth_port = 8080
2740 auth_ssl = no
2741+# For Swauth:
2742+# auth_prefix = /auth/
2743
2744 # Primary functional test account (needs admin access to the account)
2745 account = test
2746
2747=== modified file 'test/functional/swift.py'
2748--- test/functional/swift.py 2011-01-04 23:34:43 +0000
2749+++ test/functional/swift.py 2011-01-10 20:41:38 +0000
2750@@ -82,6 +82,7 @@
2751 self.auth_host = config['auth_host']
2752 self.auth_port = int(config['auth_port'])
2753 self.auth_ssl = config['auth_ssl'] in ('on', 'true', 'yes', '1')
2754+ self.auth_prefix = config.get('auth_prefix', '/')
2755
2756 self.account = config['account']
2757 self.username = config['username']
2758@@ -105,11 +106,11 @@
2759 return
2760
2761 headers = {
2762- 'x-storage-user': self.username,
2763- 'x-storage-pass': self.password,
2764+ 'x-auth-user': '%s:%s' % (self.account, self.username),
2765+ 'x-auth-key': self.password,
2766 }
2767
2768- path = '/v1/%s/auth' % (self.account)
2769+ path = '%sv1.0' % (self.auth_prefix)
2770 if self.auth_ssl:
2771 connection = httplib.HTTPSConnection(self.auth_host,
2772 port=self.auth_port)
2773
2774=== modified file 'test/functionalnosetests/swift_testing.py'
2775--- test/functionalnosetests/swift_testing.py 2010-09-09 17:24:25 +0000
2776+++ test/functionalnosetests/swift_testing.py 2011-01-10 20:41:38 +0000
2777@@ -31,7 +31,10 @@
2778 swift_test_auth = 'http'
2779 if conf.get('auth_ssl', 'no').lower() in ('yes', 'true', 'on', '1'):
2780 swift_test_auth = 'https'
2781- swift_test_auth += '://%(auth_host)s:%(auth_port)s/v1.0' % conf
2782+ if 'auth_prefix' not in conf:
2783+ conf['auth_prefix'] = '/'
2784+ swift_test_auth += \
2785+ '://%(auth_host)s:%(auth_port)s%(auth_prefix)sv1.0' % conf
2786 swift_test_user[0] = '%(account)s:%(username)s' % conf
2787 swift_test_key[0] = conf['password']
2788 try:
2789
2790=== modified file 'test/probe/common.py'
2791--- test/probe/common.py 2011-01-04 23:34:43 +0000
2792+++ test/probe/common.py 2011-01-10 20:41:38 +0000
2793@@ -24,13 +24,25 @@
2794 from swift.common.ring import Ring
2795
2796
2797+SUPER_ADMIN_KEY = None
2798+AUTH_TYPE = None
2799+
2800+c = ConfigParser()
2801 AUTH_SERVER_CONF_FILE = environ.get('SWIFT_AUTH_SERVER_CONF_FILE',
2802 '/etc/swift/auth-server.conf')
2803-c = ConfigParser()
2804-if not c.read(AUTH_SERVER_CONF_FILE):
2805- exit('Unable to read config file: %s' % AUTH_SERVER_CONF_FILE)
2806-conf = dict(c.items('app:auth-server'))
2807-SUPER_ADMIN_KEY = conf.get('super_admin_key', 'devauth')
2808+if c.read(AUTH_SERVER_CONF_FILE):
2809+ conf = dict(c.items('app:auth-server'))
2810+ SUPER_ADMIN_KEY = conf.get('super_admin_key', 'devauth')
2811+ AUTH_TYPE = 'devauth'
2812+else:
2813+ PROXY_SERVER_CONF_FILE = environ.get('SWIFT_PROXY_SERVER_CONF_FILE',
2814+ '/etc/swift/proxy-server.conf')
2815+ if c.read(PROXY_SERVER_CONF_FILE):
2816+ conf = dict(c.items('filter:swauth'))
2817+ SUPER_ADMIN_KEY = conf.get('super_admin_key', 'swauthkey')
2818+ AUTH_TYPE = 'swauth'
2819+ else:
2820+ exit('Unable to read config file: %s' % AUTH_SERVER_CONF_FILE)
2821
2822
2823 def kill_pids(pids):
2824@@ -45,8 +57,9 @@
2825 call(['resetswift'])
2826 pids = {}
2827 try:
2828- pids['auth'] = Popen(['swift-auth-server',
2829- '/etc/swift/auth-server.conf']).pid
2830+ if AUTH_TYPE == 'devauth':
2831+ pids['auth'] = Popen(['swift-auth-server',
2832+ '/etc/swift/auth-server.conf']).pid
2833 pids['proxy'] = Popen(['swift-proxy-server',
2834 '/etc/swift/proxy-server.conf']).pid
2835 port2server = {}
2836@@ -60,14 +73,21 @@
2837 container_ring = Ring('/etc/swift/container.ring.gz')
2838 object_ring = Ring('/etc/swift/object.ring.gz')
2839 sleep(5)
2840- conn = http_connect('127.0.0.1', '11000', 'POST', '/recreate_accounts',
2841- headers={'X-Auth-Admin-User': '.super_admin',
2842- 'X-Auth-Admin-Key': SUPER_ADMIN_KEY})
2843- resp = conn.getresponse()
2844- if resp.status != 200:
2845- raise Exception('Recreating accounts failed. (%d)' % resp.status)
2846- url, token = \
2847- get_auth('http://127.0.0.1:11000/auth', 'test:tester', 'testing')
2848+ if AUTH_TYPE == 'devauth':
2849+ conn = http_connect('127.0.0.1', '11000', 'POST',
2850+ '/recreate_accounts',
2851+ headers={'X-Auth-Admin-User': '.super_admin',
2852+ 'X-Auth-Admin-Key': SUPER_ADMIN_KEY})
2853+ resp = conn.getresponse()
2854+ if resp.status != 200:
2855+ raise Exception('Recreating accounts failed. (%d)' %
2856+ resp.status)
2857+ url, token = get_auth('http://127.0.0.1:11000/auth', 'test:tester',
2858+ 'testing')
2859+ elif AUTH_TYPE == 'swauth':
2860+ call(['recreateaccounts'])
2861+ url, token = get_auth('http://127.0.0.1:8080/auth/v1.0',
2862+ 'test:tester', 'testing')
2863 account = url.split('/')[-1]
2864 except BaseException, err:
2865 kill_pids(pids)
2866
2867=== modified file 'test/unit/common/middleware/test_auth.py'
2868--- test/unit/common/middleware/test_auth.py 2011-01-04 23:34:43 +0000
2869+++ test/unit/common/middleware/test_auth.py 2011-01-10 20:41:38 +0000
2870@@ -432,6 +432,40 @@
2871 resp = self.test_auth.authorize(req)
2872 self.assertEquals(resp and resp.status_int, 403)
2873
2874+ def test_account_delete_permissions(self):
2875+ req = Request.blank('/v1/AUTH_new',
2876+ environ={'REQUEST_METHOD': 'DELETE'})
2877+ req.remote_user = 'act:usr,act'
2878+ resp = self.test_auth.authorize(req)
2879+ self.assertEquals(resp and resp.status_int, 403)
2880+
2881+ req = Request.blank('/v1/AUTH_new',
2882+ environ={'REQUEST_METHOD': 'DELETE'})
2883+ req.remote_user = 'act:usr,act,AUTH_other'
2884+ resp = self.test_auth.authorize(req)
2885+ self.assertEquals(resp and resp.status_int, 403)
2886+
2887+ # Even DELETEs to your own account as account admin should fail
2888+ req = Request.blank('/v1/AUTH_old',
2889+ environ={'REQUEST_METHOD': 'DELETE'})
2890+ req.remote_user = 'act:usr,act,AUTH_old'
2891+ resp = self.test_auth.authorize(req)
2892+ self.assertEquals(resp and resp.status_int, 403)
2893+
2894+ req = Request.blank('/v1/AUTH_new',
2895+ environ={'REQUEST_METHOD': 'DELETE'})
2896+ req.remote_user = 'act:usr,act,.reseller_admin'
2897+ resp = self.test_auth.authorize(req)
2898+ self.assertEquals(resp, None)
2899+
2900+ # .super_admin is not something the middleware should ever see or care
2901+ # about
2902+ req = Request.blank('/v1/AUTH_new',
2903+ environ={'REQUEST_METHOD': 'DELETE'})
2904+ req.remote_user = 'act:usr,act,.super_admin'
2905+ resp = self.test_auth.authorize(req)
2906+ self.assertEquals(resp and resp.status_int, 403)
2907+
2908
2909 if __name__ == '__main__':
2910 unittest.main()
2911
2912=== added file 'test/unit/common/middleware/test_swauth.py'
2913--- test/unit/common/middleware/test_swauth.py 1970-01-01 00:00:00 +0000
2914+++ test/unit/common/middleware/test_swauth.py 2011-01-10 20:41:38 +0000
2915@@ -0,0 +1,3117 @@
2916+# Copyright (c) 2010 OpenStack, LLC.
2917+#
2918+# Licensed under the Apache License, Version 2.0 (the "License");
2919+# you may not use this file except in compliance with the License.
2920+# You may obtain a copy of the License at
2921+#
2922+# http://www.apache.org/licenses/LICENSE-2.0
2923+#
2924+# Unless required by applicable law or agreed to in writing, software
2925+# distributed under the License is distributed on an "AS IS" BASIS,
2926+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
2927+# implied.
2928+# See the License for the specific language governing permissions and
2929+# limitations under the License.
2930+
2931+try:
2932+ import simplejson as json
2933+except ImportError:
2934+ import json
2935+import unittest
2936+from contextlib import contextmanager
2937+from time import time
2938+
2939+from webob import Request, Response
2940+
2941+from swift.common.middleware import swauth as auth
2942+
2943+
2944+class FakeMemcache(object):
2945+
2946+ def __init__(self):
2947+ self.store = {}
2948+
2949+ def get(self, key):
2950+ return self.store.get(key)
2951+
2952+ def set(self, key, value, timeout=0):
2953+ self.store[key] = value
2954+ return True
2955+
2956+ def incr(self, key, timeout=0):
2957+ self.store[key] = self.store.setdefault(key, 0) + 1
2958+ return self.store[key]
2959+
2960+ @contextmanager
2961+ def soft_lock(self, key, timeout=0, retries=5):
2962+ yield True
2963+
2964+ def delete(self, key):
2965+ try:
2966+ del self.store[key]
2967+ except:
2968+ pass
2969+ return True
2970+
2971+
2972+class FakeApp(object):
2973+
2974+ def __init__(self, status_headers_body_iter=None):
2975+ self.calls = 0
2976+ self.status_headers_body_iter = status_headers_body_iter
2977+ if not self.status_headers_body_iter:
2978+ self.status_headers_body_iter = iter([('404 Not Found', {}, '')])
2979+
2980+ def __call__(self, env, start_response):
2981+ self.calls += 1
2982+ self.request = Request.blank('', environ=env)
2983+ if 'swift.authorize' in env:
2984+ resp = env['swift.authorize'](self.request)
2985+ if resp:
2986+ return resp(env, start_response)
2987+ status, headers, body = self.status_headers_body_iter.next()
2988+ return Response(status=status, headers=headers,
2989+ body=body)(env, start_response)
2990+
2991+
2992+class FakeConn(object):
2993+
2994+ def __init__(self, status_headers_body_iter=None):
2995+ self.calls = 0
2996+ self.status_headers_body_iter = status_headers_body_iter
2997+ if not self.status_headers_body_iter:
2998+ self.status_headers_body_iter = iter([('404 Not Found', {}, '')])
2999+
3000+ def request(self, method, path, headers):
3001+ self.calls += 1
3002+ self.request_path = path
3003+ self.status, self.headers, self.body = \
3004+ self.status_headers_body_iter.next()
3005+ self.status, self.reason = self.status.split(' ', 1)
3006+ self.status = int(self.status)
3007+
3008+ def getresponse(self):
3009+ return self
3010+
3011+ def read(self):
3012+ body = self.body
3013+ self.body = ''
3014+ return body
3015+
3016+
3017+class TestAuth(unittest.TestCase):
3018+
3019+ def setUp(self):
3020+ self.test_auth = \
3021+ auth.filter_factory({'super_admin_key': 'supertest'})(FakeApp())
3022+
3023+ def test_super_admin_key_required(self):
3024+ app = FakeApp()
3025+ exc = None
3026+ try:
3027+ auth.filter_factory({})(app)
3028+ except ValueError, err:
3029+ exc = err
3030+ self.assertEquals(str(exc),
3031+ 'No super_admin_key set in conf file! Exiting.')
3032+ auth.filter_factory({'super_admin_key': 'supertest'})(app)
3033+
3034+ def test_reseller_prefix_init(self):
3035+ app = FakeApp()
3036+ ath = auth.filter_factory({'super_admin_key': 'supertest'})(app)
3037+ self.assertEquals(ath.reseller_prefix, 'AUTH_')
3038+ ath = auth.filter_factory({'super_admin_key': 'supertest',
3039+ 'reseller_prefix': 'TEST'})(app)
3040+ self.assertEquals(ath.reseller_prefix, 'TEST_')
3041+ ath = auth.filter_factory({'super_admin_key': 'supertest',
3042+ 'reseller_prefix': 'TEST_'})(app)
3043+ self.assertEquals(ath.reseller_prefix, 'TEST_')
3044+
3045+ def test_auth_prefix_init(self):
3046+ app = FakeApp()
3047+ ath = auth.filter_factory({'super_admin_key': 'supertest'})(app)
3048+ self.assertEquals(ath.auth_prefix, '/auth/')
3049+ ath = auth.filter_factory({'super_admin_key': 'supertest',
3050+ 'auth_prefix': ''})(app)
3051+ self.assertEquals(ath.auth_prefix, '/auth/')
3052+ ath = auth.filter_factory({'super_admin_key': 'supertest',
3053+ 'auth_prefix': '/test/'})(app)
3054+ self.assertEquals(ath.auth_prefix, '/test/')
3055+ ath = auth.filter_factory({'super_admin_key': 'supertest',
3056+ 'auth_prefix': '/test'})(app)
3057+ self.assertEquals(ath.auth_prefix, '/test/')
3058+ ath = auth.filter_factory({'super_admin_key': 'supertest',
3059+ 'auth_prefix': 'test/'})(app)
3060+ self.assertEquals(ath.auth_prefix, '/test/')
3061+ ath = auth.filter_factory({'super_admin_key': 'supertest',
3062+ 'auth_prefix': 'test'})(app)
3063+ self.assertEquals(ath.auth_prefix, '/test/')
3064+
3065+ def test_default_swift_cluster_init(self):
3066+ app = FakeApp()
3067+ self.assertRaises(Exception, auth.filter_factory({
3068+ 'super_admin_key': 'supertest',
3069+ 'default_swift_cluster': 'local:badscheme://host/path'}), app)
3070+ ath = auth.filter_factory({'super_admin_key': 'supertest'})(app)
3071+ self.assertEquals(ath.default_swift_cluster,
3072+ 'local:http://127.0.0.1:8080/v1')
3073+ ath = auth.filter_factory({'super_admin_key': 'supertest',
3074+ 'default_swift_cluster': 'local:http://host/path'})(app)
3075+ self.assertEquals(ath.default_swift_cluster,
3076+ 'local:http://host/path')
3077+ ath = auth.filter_factory({'super_admin_key': 'supertest',
3078+ 'default_swift_cluster': 'local:https://host/path/'})(app)
3079+ self.assertEquals(ath.dsc_url, 'https://host/path')
3080+ self.assertEquals(ath.dsc_url2, 'https://host/path')
3081+ ath = auth.filter_factory({'super_admin_key': 'supertest',
3082+ 'default_swift_cluster':
3083+ 'local::https://host/path/::http://host2/path2/'})(app)
3084+ self.assertEquals(ath.dsc_url, 'https://host/path')
3085+ self.assertEquals(ath.dsc_url2, 'http://host2/path2')
3086+
3087+ def test_top_level_ignore(self):
3088+ resp = Request.blank('/').get_response(self.test_auth)
3089+ self.assertEquals(resp.status_int, 404)
3090+
3091+ def test_anon(self):
3092+ resp = Request.blank('/v1/AUTH_account').get_response(self.test_auth)
3093+ self.assertEquals(resp.status_int, 401)
3094+ self.assertEquals(resp.environ['swift.authorize'],
3095+ self.test_auth.authorize)
3096+
3097+ def test_auth_deny_non_reseller_prefix(self):
3098+ resp = Request.blank('/v1/BLAH_account',
3099+ headers={'X-Auth-Token': 'BLAH_t'}).get_response(self.test_auth)
3100+ self.assertEquals(resp.status_int, 401)
3101+ self.assertEquals(resp.environ['swift.authorize'],
3102+ self.test_auth.denied_response)
3103+
3104+ def test_auth_deny_non_reseller_prefix_no_override(self):
3105+ fake_authorize = lambda x: Response(status='500 Fake')
3106+ resp = Request.blank('/v1/BLAH_account',
3107+ headers={'X-Auth-Token': 'BLAH_t'},
3108+ environ={'swift.authorize': fake_authorize}
3109+ ).get_response(self.test_auth)
3110+ self.assertEquals(resp.status_int, 500)
3111+ self.assertEquals(resp.environ['swift.authorize'], fake_authorize)
3112+
3113+ def test_auth_no_reseller_prefix_deny(self):
3114+ # Ensures that when we have no reseller prefix, we don't deny a request
3115+ # outright but set up a denial swift.authorize and pass the request on
3116+ # down the chain.
3117+ local_app = FakeApp()
3118+ local_auth = auth.filter_factory({'super_admin_key': 'supertest',
3119+ 'reseller_prefix': ''})(local_app)
3120+ resp = Request.blank('/v1/account',
3121+ headers={'X-Auth-Token': 't'}).get_response(local_auth)
3122+ self.assertEquals(resp.status_int, 401)
3123+ # one for checking auth, two for request passed along
3124+ self.assertEquals(local_app.calls, 2)
3125+ self.assertEquals(resp.environ['swift.authorize'],
3126+ local_auth.denied_response)
3127+
3128+ def test_auth_no_reseller_prefix_allow(self):
3129+ # Ensures that when we have no reseller prefix, we can still allow
3130+ # access if our auth server accepts requests
3131+ local_app = FakeApp(iter([
3132+ ('200 Ok', {},
3133+ json.dumps({'account': 'act', 'user': 'act:usr',
3134+ 'account_id': 'AUTH_cfa',
3135+ 'groups': [{'name': 'act:usr'}, {'name': 'act'},
3136+ {'name': '.admin'}],
3137+ 'expires': time() + 60})),
3138+ ('204 No Content', {}, '')]))
3139+ local_auth = auth.filter_factory({'super_admin_key': 'supertest',
3140+ 'reseller_prefix': ''})(local_app)
3141+ resp = Request.blank('/v1/act',
3142+ headers={'X-Auth-Token': 't'}).get_response(local_auth)
3143+ self.assertEquals(resp.status_int, 204)
3144+ self.assertEquals(local_app.calls, 2)
3145+ self.assertEquals(resp.environ['swift.authorize'],
3146+ local_auth.authorize)
3147+
3148+ def test_auth_no_reseller_prefix_no_token(self):
3149+ # Check that normally we set up a call back to our authorize.
3150+ local_auth = \
3151+ auth.filter_factory({'super_admin_key': 'supertest',
3152+ 'reseller_prefix': ''})(FakeApp(iter([])))
3153+ resp = Request.blank('/v1/account').get_response(local_auth)
3154+ self.assertEquals(resp.status_int, 401)
3155+ self.assertEquals(resp.environ['swift.authorize'],
3156+ local_auth.authorize)
3157+ # Now make sure we don't override an existing swift.authorize when we
3158+ # have no reseller prefix.
3159+ local_auth = \
3160+ auth.filter_factory({'super_admin_key': 'supertest',
3161+ 'reseller_prefix': ''})(FakeApp())
3162+ local_authorize = lambda req: Response('test')
3163+ resp = Request.blank('/v1/account', environ={'swift.authorize':
3164+ local_authorize}).get_response(local_auth)
3165+ self.assertEquals(resp.status_int, 200)
3166+ self.assertEquals(resp.environ['swift.authorize'], local_authorize)
3167+
3168+ def test_auth_fail(self):
3169+ resp = Request.blank('/v1/AUTH_cfa',
3170+ headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth)
3171+ self.assertEquals(resp.status_int, 401)
3172+
3173+ def test_auth_success(self):
3174+ self.test_auth.app = FakeApp(iter([
3175+ ('200 Ok', {},
3176+ json.dumps({'account': 'act', 'user': 'act:usr',
3177+ 'account_id': 'AUTH_cfa',
3178+ 'groups': [{'name': 'act:usr'}, {'name': 'act'},
3179+ {'name': '.admin'}],
3180+ 'expires': time() + 60})),
3181+ ('204 No Content', {}, '')]))
3182+ resp = Request.blank('/v1/AUTH_cfa',
3183+ headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth)
3184+ self.assertEquals(resp.status_int, 204)
3185+ self.assertEquals(self.test_auth.app.calls, 2)
3186+
3187+ def test_auth_memcache(self):
3188+ # First run our test without memcache, showing we need to return the
3189+ # token contents twice.
3190+ self.test_auth.app = FakeApp(iter([
3191+ ('200 Ok', {},
3192+ json.dumps({'account': 'act', 'user': 'act:usr',
3193+ 'account_id': 'AUTH_cfa',
3194+ 'groups': [{'name': 'act:usr'}, {'name': 'act'},
3195+ {'name': '.admin'}],
3196+ 'expires': time() + 60})),
3197+ ('204 No Content', {}, ''),
3198+ ('200 Ok', {},
3199+ json.dumps({'account': 'act', 'user': 'act:usr',
3200+ 'account_id': 'AUTH_cfa',
3201+ 'groups': [{'name': 'act:usr'}, {'name': 'act'},
3202+ {'name': '.admin'}],
3203+ 'expires': time() + 60})),
3204+ ('204 No Content', {}, '')]))
3205+ resp = Request.blank('/v1/AUTH_cfa',
3206+ headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth)
3207+ self.assertEquals(resp.status_int, 204)
3208+ resp = Request.blank('/v1/AUTH_cfa',
3209+ headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth)
3210+ self.assertEquals(resp.status_int, 204)
3211+ self.assertEquals(self.test_auth.app.calls, 4)
3212+ # Now run our test with memcache, showing we no longer need to return
3213+ # the token contents twice.
3214+ self.test_auth.app = FakeApp(iter([
3215+ ('200 Ok', {},
3216+ json.dumps({'account': 'act', 'user': 'act:usr',
3217+ 'account_id': 'AUTH_cfa',
3218+ 'groups': [{'name': 'act:usr'}, {'name': 'act'},
3219+ {'name': '.admin'}],
3220+ 'expires': time() + 60})),
3221+ ('204 No Content', {}, ''),
3222+ # Don't need a second token object returned if memcache is used
3223+ ('204 No Content', {}, '')]))
3224+ fake_memcache = FakeMemcache()
3225+ resp = Request.blank('/v1/AUTH_cfa',
3226+ headers={'X-Auth-Token': 'AUTH_t'},
3227+ environ={'swift.cache': fake_memcache}
3228+ ).get_response(self.test_auth)
3229+ self.assertEquals(resp.status_int, 204)
3230+ resp = Request.blank('/v1/AUTH_cfa',
3231+ headers={'X-Auth-Token': 'AUTH_t'},
3232+ environ={'swift.cache': fake_memcache}
3233+ ).get_response(self.test_auth)
3234+ self.assertEquals(resp.status_int, 204)
3235+ self.assertEquals(self.test_auth.app.calls, 3)
3236+
3237+ def test_auth_just_expired(self):
3238+ self.test_auth.app = FakeApp(iter([
3239+ # Request for token (which will have expired)
3240+ ('200 Ok', {},
3241+ json.dumps({'account': 'act', 'user': 'act:usr',
3242+ 'account_id': 'AUTH_cfa',
3243+ 'groups': [{'name': 'act:usr'}, {'name': 'act'},
3244+ {'name': '.admin'}],
3245+ 'expires': time() - 1})),
3246+ # Request to delete token
3247+ ('204 No Content', {}, '')]))
3248+ resp = Request.blank('/v1/AUTH_cfa',
3249+ headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth)
3250+ self.assertEquals(resp.status_int, 401)
3251+ self.assertEquals(self.test_auth.app.calls, 2)
3252+
3253+ def test_middleware_storage_token(self):
3254+ self.test_auth.app = FakeApp(iter([
3255+ ('200 Ok', {},
3256+ json.dumps({'account': 'act', 'user': 'act:usr',
3257+ 'account_id': 'AUTH_cfa',
3258+ 'groups': [{'name': 'act:usr'}, {'name': 'act'},
3259+ {'name': '.admin'}],
3260+ 'expires': time() + 60})),
3261+ ('204 No Content', {}, '')]))
3262+ resp = Request.blank('/v1/AUTH_cfa',
3263+ headers={'X-Storage-Token': 'AUTH_t'}).get_response(self.test_auth)
3264+ self.assertEquals(resp.status_int, 204)
3265+ self.assertEquals(self.test_auth.app.calls, 2)
3266+
3267+ def test_authorize_bad_path(self):
3268+ req = Request.blank('/badpath')
3269+ resp = self.test_auth.authorize(req)
3270+ self.assertEquals(resp.status_int, 401)
3271+ req = Request.blank('/badpath')
3272+ req.remote_user = 'act:usr,act,AUTH_cfa'
3273+ resp = self.test_auth.authorize(req)
3274+ self.assertEquals(resp.status_int, 403)
3275+
3276+ def test_authorize_account_access(self):
3277+ req = Request.blank('/v1/AUTH_cfa')
3278+ req.remote_user = 'act:usr,act,AUTH_cfa'
3279+ self.assertEquals(self.test_auth.authorize(req), None)
3280+ req = Request.blank('/v1/AUTH_cfa')
3281+ req.remote_user = 'act:usr,act'
3282+ resp = self.test_auth.authorize(req)
3283+ self.assertEquals(resp.status_int, 403)
3284+
3285+ def test_authorize_acl_group_access(self):
3286+ req = Request.blank('/v1/AUTH_cfa')
3287+ req.remote_user = 'act:usr,act'
3288+ resp = self.test_auth.authorize(req)
3289+ self.assertEquals(resp.status_int, 403)
3290+ req = Request.blank('/v1/AUTH_cfa')
3291+ req.remote_user = 'act:usr,act'
3292+ req.acl = 'act'
3293+ self.assertEquals(self.test_auth.authorize(req), None)
3294+ req = Request.blank('/v1/AUTH_cfa')
3295+ req.remote_user = 'act:usr,act'
3296+ req.acl = 'act:usr'
3297+ self.assertEquals(self.test_auth.authorize(req), None)
3298+ req = Request.blank('/v1/AUTH_cfa')
3299+ req.remote_user = 'act:usr,act'
3300+ req.acl = 'act2'
3301+ resp = self.test_auth.authorize(req)
3302+ self.assertEquals(resp.status_int, 403)
3303+ req = Request.blank('/v1/AUTH_cfa')
3304+ req.remote_user = 'act:usr,act'
3305+ req.acl = 'act:usr2'
3306+ resp = self.test_auth.authorize(req)
3307+ self.assertEquals(resp.status_int, 403)
3308+
3309+ def test_deny_cross_reseller(self):
3310+ # Tests that cross-reseller is denied, even if ACLs/group names match
3311+ req = Request.blank('/v1/OTHER_cfa')
3312+ req.remote_user = 'act:usr,act,AUTH_cfa'
3313+ req.acl = 'act'
3314+ resp = self.test_auth.authorize(req)
3315+ self.assertEquals(resp.status_int, 403)
3316+
3317+ def test_authorize_acl_referrer_access(self):
3318+ req = Request.blank('/v1/AUTH_cfa')
3319+ req.remote_user = 'act:usr,act'
3320+ resp = self.test_auth.authorize(req)
3321+ self.assertEquals(resp.status_int, 403)
3322+ req = Request.blank('/v1/AUTH_cfa')
3323+ req.remote_user = 'act:usr,act'
3324+ req.acl = '.r:*'
3325+ self.assertEquals(self.test_auth.authorize(req), None)
3326+ req = Request.blank('/v1/AUTH_cfa')
3327+ req.remote_user = 'act:usr,act'
3328+ req.acl = '.r:.example.com'
3329+ resp = self.test_auth.authorize(req)
3330+ self.assertEquals(resp.status_int, 403)
3331+ req = Request.blank('/v1/AUTH_cfa')
3332+ req.remote_user = 'act:usr,act'
3333+ req.referer = 'http://www.example.com/index.html'
3334+ req.acl = '.r:.example.com'
3335+ self.assertEquals(self.test_auth.authorize(req), None)
3336+ req = Request.blank('/v1/AUTH_cfa')
3337+ resp = self.test_auth.authorize(req)
3338+ self.assertEquals(resp.status_int, 401)
3339+ req = Request.blank('/v1/AUTH_cfa')
3340+ req.acl = '.r:*'
3341+ self.assertEquals(self.test_auth.authorize(req), None)
3342+ req = Request.blank('/v1/AUTH_cfa')
3343+ req.acl = '.r:.example.com'
3344+ resp = self.test_auth.authorize(req)
3345+ self.assertEquals(resp.status_int, 401)
3346+ req = Request.blank('/v1/AUTH_cfa')
3347+ req.referer = 'http://www.example.com/index.html'
3348+ req.acl = '.r:.example.com'
3349+ self.assertEquals(self.test_auth.authorize(req), None)
3350+
3351+ def test_account_put_permissions(self):
3352+ req = Request.blank('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'})
3353+ req.remote_user = 'act:usr,act'
3354+ resp = self.test_auth.authorize(req)
3355+ self.assertEquals(resp.status_int, 403)
3356+
3357+ req = Request.blank('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'})
3358+ req.remote_user = 'act:usr,act,AUTH_other'
3359+ resp = self.test_auth.authorize(req)
3360+ self.assertEquals(resp.status_int, 403)
3361+
3362+ # Even PUTs to your own account as account admin should fail
3363+ req = Request.blank('/v1/AUTH_old', environ={'REQUEST_METHOD': 'PUT'})
3364+ req.remote_user = 'act:usr,act,AUTH_old'
3365+ resp = self.test_auth.authorize(req)
3366+ self.assertEquals(resp.status_int, 403)
3367+
3368+ req = Request.blank('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'})
3369+ req.remote_user = 'act:usr,act,.reseller_admin'
3370+ resp = self.test_auth.authorize(req)
3371+ self.assertEquals(resp, None)
3372+
3373+ # .super_admin is not something the middleware should ever see or care
3374+ # about
3375+ req = Request.blank('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'})
3376+ req.remote_user = 'act:usr,act,.super_admin'
3377+ resp = self.test_auth.authorize(req)
3378+ self.assertEquals(resp.status_int, 403)
3379+
3380+ def test_account_delete_permissions(self):
3381+ req = Request.blank('/v1/AUTH_new',
3382+ environ={'REQUEST_METHOD': 'DELETE'})
3383+ req.remote_user = 'act:usr,act'
3384+ resp = self.test_auth.authorize(req)
3385+ self.assertEquals(resp.status_int, 403)
3386+
3387+ req = Request.blank('/v1/AUTH_new',
3388+ environ={'REQUEST_METHOD': 'DELETE'})
3389+ req.remote_user = 'act:usr,act,AUTH_other'
3390+ resp = self.test_auth.authorize(req)
3391+ self.assertEquals(resp.status_int, 403)
3392+
3393+ # Even DELETEs to your own account as account admin should fail
3394+ req = Request.blank('/v1/AUTH_old',
3395+ environ={'REQUEST_METHOD': 'DELETE'})
3396+ req.remote_user = 'act:usr,act,AUTH_old'
3397+ resp = self.test_auth.authorize(req)
3398+ self.assertEquals(resp.status_int, 403)
3399+
3400+ req = Request.blank('/v1/AUTH_new',
3401+ environ={'REQUEST_METHOD': 'DELETE'})
3402+ req.remote_user = 'act:usr,act,.reseller_admin'
3403+ resp = self.test_auth.authorize(req)
3404+ self.assertEquals(resp, None)
3405+
3406+ # .super_admin is not something the middleware should ever see or care
3407+ # about
3408+ req = Request.blank('/v1/AUTH_new',
3409+ environ={'REQUEST_METHOD': 'DELETE'})
3410+ req.remote_user = 'act:usr,act,.super_admin'
3411+ resp = self.test_auth.authorize(req)
3412+ resp = self.test_auth.authorize(req)
3413+ self.assertEquals(resp.status_int, 403)
3414+
3415+ def test_get_token_fail(self):
3416+ resp = Request.blank('/auth/v1.0').get_response(self.test_auth)
3417+ self.assertEquals(resp.status_int, 401)
3418+ resp = Request.blank('/auth/v1.0',
3419+ headers={'X-Auth-User': 'act:usr',
3420+ 'X-Auth-Key': 'key'}).get_response(self.test_auth)
3421+ self.assertEquals(resp.status_int, 401)
3422+
3423+ def test_get_token_fail_invalid_key(self):
3424+ self.test_auth.app = FakeApp(iter([
3425+ # GET of user object
3426+ ('200 Ok', {},
3427+ json.dumps({"auth": "plaintext:key",
3428+ "groups": [{'name': "act:usr"}, {'name': "act"},
3429+ {'name': ".admin"}]}))]))
3430+ resp = Request.blank('/auth/v1.0',
3431+ headers={'X-Auth-User': 'act:usr',
3432+ 'X-Auth-Key': 'invalid'}).get_response(self.test_auth)
3433+ self.assertEquals(resp.status_int, 401)
3434+ self.assertEquals(self.test_auth.app.calls, 1)
3435+
3436+ def test_get_token_fail_invalid_x_auth_user_format(self):
3437+ resp = Request.blank('/auth/v1/act/auth',
3438+ headers={'X-Auth-User': 'usr',
3439+ 'X-Auth-Key': 'key'}).get_response(self.test_auth)
3440+ self.assertEquals(resp.status_int, 401)
3441+
3442+ def test_get_token_fail_non_matching_account_in_request(self):
3443+ resp = Request.blank('/auth/v1/act/auth',
3444+ headers={'X-Auth-User': 'act2:usr',
3445+ 'X-Auth-Key': 'key'}).get_response(self.test_auth)
3446+ self.assertEquals(resp.status_int, 401)
3447+
3448+ def test_get_token_fail_bad_path(self):
3449+ resp = Request.blank('/auth/v1/act/auth/invalid',
3450+ headers={'X-Auth-User': 'act:usr',
3451+ 'X-Auth-Key': 'key'}).get_response(self.test_auth)
3452+ self.assertEquals(resp.status_int, 400)
3453+
3454+ def test_get_token_fail_missing_key(self):
3455+ resp = Request.blank('/auth/v1/act/auth',
3456+ headers={'X-Auth-User': 'act:usr'}).get_response(self.test_auth)
3457+ self.assertEquals(resp.status_int, 401)
3458+
3459+ def test_get_token_fail_get_user_details(self):
3460+ self.test_auth.app = FakeApp(iter([
3461+ ('503 Service Unavailable', {}, '')]))
3462+ resp = Request.blank('/auth/v1.0',
3463+ headers={'X-Auth-User': 'act:usr',
3464+ 'X-Auth-Key': 'key'}).get_response(self.test_auth)
3465+ self.assertEquals(resp.status_int, 500)
3466+ self.assertEquals(self.test_auth.app.calls, 1)
3467+
3468+ def test_get_token_fail_get_account(self):
3469+ self.test_auth.app = FakeApp(iter([
3470+ # GET of user object
3471+ ('200 Ok', {},
3472+ json.dumps({"auth": "plaintext:key",
3473+ "groups": [{'name': "act:usr"}, {'name': "act"},
3474+ {'name': ".admin"}]})),
3475+ # GET of account
3476+ ('503 Service Unavailable', {}, '')]))
3477+ resp = Request.blank('/auth/v1.0',
3478+ headers={'X-Auth-User': 'act:usr',
3479+ 'X-Auth-Key': 'key'}).get_response(self.test_auth)
3480+ self.assertEquals(resp.status_int, 500)
3481+ self.assertEquals(self.test_auth.app.calls, 2)
3482+
3483+ def test_get_token_fail_put_new_token(self):
3484+ self.test_auth.app = FakeApp(iter([
3485+ # GET of user object
3486+ ('200 Ok', {},
3487+ json.dumps({"auth": "plaintext:key",
3488+ "groups": [{'name': "act:usr"}, {'name': "act"},
3489+ {'name': ".admin"}]})),
3490+ # GET of account
3491+ ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
3492+ # PUT of new token
3493+ ('503 Service Unavailable', {}, '')]))
3494+ resp = Request.blank('/auth/v1.0',
3495+ headers={'X-Auth-User': 'act:usr',
3496+ 'X-Auth-Key': 'key'}).get_response(self.test_auth)
3497+ self.assertEquals(resp.status_int, 500)
3498+ self.assertEquals(self.test_auth.app.calls, 3)
3499+
3500+ def test_get_token_fail_post_to_user(self):
3501+ self.test_auth.app = FakeApp(iter([
3502+ # GET of user object
3503+ ('200 Ok', {},
3504+ json.dumps({"auth": "plaintext:key",
3505+ "groups": [{'name': "act:usr"}, {'name': "act"},
3506+ {'name': ".admin"}]})),
3507+ # GET of account
3508+ ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
3509+ # PUT of new token
3510+ ('201 Created', {}, ''),
3511+ # POST of token to user object
3512+ ('503 Service Unavailable', {}, '')]))
3513+ resp = Request.blank('/auth/v1.0',
3514+ headers={'X-Auth-User': 'act:usr',
3515+ 'X-Auth-Key': 'key'}).get_response(self.test_auth)
3516+ self.assertEquals(resp.status_int, 500)
3517+ self.assertEquals(self.test_auth.app.calls, 4)
3518+
3519+ def test_get_token_fail_get_services(self):
3520+ self.test_auth.app = FakeApp(iter([
3521+ # GET of user object
3522+ ('200 Ok', {},
3523+ json.dumps({"auth": "plaintext:key",
3524+ "groups": [{'name': "act:usr"}, {'name': "act"},
3525+ {'name': ".admin"}]})),
3526+ # GET of account
3527+ ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
3528+ # PUT of new token
3529+ ('201 Created', {}, ''),
3530+ # POST of token to user object
3531+ ('204 No Content', {}, ''),
3532+ # GET of services object
3533+ ('503 Service Unavailable', {}, '')]))
3534+ resp = Request.blank('/auth/v1.0',
3535+ headers={'X-Auth-User': 'act:usr',
3536+ 'X-Auth-Key': 'key'}).get_response(self.test_auth)
3537+ self.assertEquals(resp.status_int, 500)
3538+ self.assertEquals(self.test_auth.app.calls, 5)
3539+
3540+ def test_get_token_fail_get_existing_token(self):
3541+ self.test_auth.app = FakeApp(iter([
3542+ # GET of user object
3543+ ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tktest'},
3544+ json.dumps({"auth": "plaintext:key",
3545+ "groups": [{'name': "act:usr"}, {'name': "act"},
3546+ {'name': ".admin"}]})),
3547+ # GET of token
3548+ ('503 Service Unavailable', {}, '')]))
3549+ resp = Request.blank('/auth/v1.0',
3550+ headers={'X-Auth-User': 'act:usr',
3551+ 'X-Auth-Key': 'key'}).get_response(self.test_auth)
3552+ self.assertEquals(resp.status_int, 500)
3553+ self.assertEquals(self.test_auth.app.calls, 2)
3554+
3555+ def test_get_token_success_v1_0(self):
3556+ self.test_auth.app = FakeApp(iter([
3557+ # GET of user object
3558+ ('200 Ok', {},
3559+ json.dumps({"auth": "plaintext:key",
3560+ "groups": [{'name': "act:usr"}, {'name': "act"},
3561+ {'name': ".admin"}]})),
3562+ # GET of account
3563+ ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
3564+ # PUT of new token
3565+ ('201 Created', {}, ''),
3566+ # POST of token to user object
3567+ ('204 No Content', {}, ''),
3568+ # GET of services object
3569+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
3570+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
3571+ resp = Request.blank('/auth/v1.0',
3572+ headers={'X-Auth-User': 'act:usr',
3573+ 'X-Auth-Key': 'key'}).get_response(self.test_auth)
3574+ self.assertEquals(resp.status_int, 200)
3575+ self.assert_(resp.headers.get('x-auth-token',
3576+ '').startswith('AUTH_tk'), resp.headers.get('x-auth-token'))
3577+ self.assertEquals(resp.headers.get('x-auth-token'),
3578+ resp.headers.get('x-storage-token'))
3579+ self.assertEquals(resp.headers.get('x-storage-url'),
3580+ 'http://127.0.0.1:8080/v1/AUTH_cfa')
3581+ self.assertEquals(json.loads(resp.body),
3582+ {"storage": {"default": "local",
3583+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
3584+ self.assertEquals(self.test_auth.app.calls, 5)
3585+
3586+ def test_get_token_success_v1_act_auth(self):
3587+ self.test_auth.app = FakeApp(iter([
3588+ # GET of user object
3589+ ('200 Ok', {},
3590+ json.dumps({"auth": "plaintext:key",
3591+ "groups": [{'name': "act:usr"}, {'name': "act"},
3592+ {'name': ".admin"}]})),
3593+ # GET of account
3594+ ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
3595+ # PUT of new token
3596+ ('201 Created', {}, ''),
3597+ # POST of token to user object
3598+ ('204 No Content', {}, ''),
3599+ # GET of services object
3600+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
3601+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
3602+ resp = Request.blank('/auth/v1/act/auth',
3603+ headers={'X-Storage-User': 'usr',
3604+ 'X-Storage-Pass': 'key'}).get_response(self.test_auth)
3605+ self.assertEquals(resp.status_int, 200)
3606+ self.assert_(resp.headers.get('x-auth-token',
3607+ '').startswith('AUTH_tk'), resp.headers.get('x-auth-token'))
3608+ self.assertEquals(resp.headers.get('x-auth-token'),
3609+ resp.headers.get('x-storage-token'))
3610+ self.assertEquals(resp.headers.get('x-storage-url'),
3611+ 'http://127.0.0.1:8080/v1/AUTH_cfa')
3612+ self.assertEquals(json.loads(resp.body),
3613+ {"storage": {"default": "local",
3614+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
3615+ self.assertEquals(self.test_auth.app.calls, 5)
3616+
3617+ def test_get_token_success_storage_instead_of_auth(self):
3618+ self.test_auth.app = FakeApp(iter([
3619+ # GET of user object
3620+ ('200 Ok', {},
3621+ json.dumps({"auth": "plaintext:key",
3622+ "groups": [{'name': "act:usr"}, {'name': "act"},
3623+ {'name': ".admin"}]})),
3624+ # GET of account
3625+ ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
3626+ # PUT of new token
3627+ ('201 Created', {}, ''),
3628+ # POST of token to user object
3629+ ('204 No Content', {}, ''),
3630+ # GET of services object
3631+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
3632+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
3633+ resp = Request.blank('/auth/v1.0',
3634+ headers={'X-Storage-User': 'act:usr',
3635+ 'X-Storage-Pass': 'key'}).get_response(self.test_auth)
3636+ self.assertEquals(resp.status_int, 200)
3637+ self.assert_(resp.headers.get('x-auth-token',
3638+ '').startswith('AUTH_tk'), resp.headers.get('x-auth-token'))
3639+ self.assertEquals(resp.headers.get('x-auth-token'),
3640+ resp.headers.get('x-storage-token'))
3641+ self.assertEquals(resp.headers.get('x-storage-url'),
3642+ 'http://127.0.0.1:8080/v1/AUTH_cfa')
3643+ self.assertEquals(json.loads(resp.body),
3644+ {"storage": {"default": "local",
3645+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
3646+ self.assertEquals(self.test_auth.app.calls, 5)
3647+
3648+ def test_get_token_success_v1_act_auth_auth_instead_of_storage(self):
3649+ self.test_auth.app = FakeApp(iter([
3650+ # GET of user object
3651+ ('200 Ok', {},
3652+ json.dumps({"auth": "plaintext:key",
3653+ "groups": [{'name': "act:usr"}, {'name': "act"},
3654+ {'name': ".admin"}]})),
3655+ # GET of account
3656+ ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
3657+ # PUT of new token
3658+ ('201 Created', {}, ''),
3659+ # POST of token to user object
3660+ ('204 No Content', {}, ''),
3661+ # GET of services object
3662+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
3663+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
3664+ resp = Request.blank('/auth/v1/act/auth',
3665+ headers={'X-Auth-User': 'act:usr',
3666+ 'X-Auth-Key': 'key'}).get_response(self.test_auth)
3667+ self.assertEquals(resp.status_int, 200)
3668+ self.assert_(resp.headers.get('x-auth-token',
3669+ '').startswith('AUTH_tk'), resp.headers.get('x-auth-token'))
3670+ self.assertEquals(resp.headers.get('x-auth-token'),
3671+ resp.headers.get('x-storage-token'))
3672+ self.assertEquals(resp.headers.get('x-storage-url'),
3673+ 'http://127.0.0.1:8080/v1/AUTH_cfa')
3674+ self.assertEquals(json.loads(resp.body),
3675+ {"storage": {"default": "local",
3676+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
3677+ self.assertEquals(self.test_auth.app.calls, 5)
3678+
3679+ def test_get_token_success_existing_token(self):
3680+ self.test_auth.app = FakeApp(iter([
3681+ # GET of user object
3682+ ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tktest'},
3683+ json.dumps({"auth": "plaintext:key",
3684+ "groups": [{'name': "act:usr"}, {'name': "act"},
3685+ {'name': ".admin"}]})),
3686+ # GET of token
3687+ ('200 Ok', {}, json.dumps({"account": "act", "user": "usr",
3688+ "account_id": "AUTH_cfa", "groups": [{'name': "act:usr"},
3689+ {'name': "key"}, {'name': ".admin"}],
3690+ "expires": 9999999999.9999999})),
3691+ # GET of services object
3692+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
3693+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
3694+ resp = Request.blank('/auth/v1.0',
3695+ headers={'X-Auth-User': 'act:usr',
3696+ 'X-Auth-Key': 'key'}).get_response(self.test_auth)
3697+ self.assertEquals(resp.status_int, 200)
3698+ self.assertEquals(resp.headers.get('x-auth-token'), 'AUTH_tktest')
3699+ self.assertEquals(resp.headers.get('x-auth-token'),
3700+ resp.headers.get('x-storage-token'))
3701+ self.assertEquals(resp.headers.get('x-storage-url'),
3702+ 'http://127.0.0.1:8080/v1/AUTH_cfa')
3703+ self.assertEquals(json.loads(resp.body),
3704+ {"storage": {"default": "local",
3705+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
3706+ self.assertEquals(self.test_auth.app.calls, 3)
3707+
3708+ def test_get_token_success_existing_token_expired(self):
3709+ self.test_auth.app = FakeApp(iter([
3710+ # GET of user object
3711+ ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tktest'},
3712+ json.dumps({"auth": "plaintext:key",
3713+ "groups": [{'name': "act:usr"}, {'name': "act"},
3714+ {'name': ".admin"}]})),
3715+ # GET of token
3716+ ('200 Ok', {}, json.dumps({"account": "act", "user": "usr",
3717+ "account_id": "AUTH_cfa", "groups": [{'name': "act:usr"},
3718+ {'name': "key"}, {'name': ".admin"}],
3719+ "expires": 0.0})),
3720+ # DELETE of expired token
3721+ ('204 No Content', {}, ''),
3722+ # GET of account
3723+ ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
3724+ # PUT of new token
3725+ ('201 Created', {}, ''),
3726+ # POST of token to user object
3727+ ('204 No Content', {}, ''),
3728+ # GET of services object
3729+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
3730+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
3731+ resp = Request.blank('/auth/v1.0',
3732+ headers={'X-Auth-User': 'act:usr',
3733+ 'X-Auth-Key': 'key'}).get_response(self.test_auth)
3734+ self.assertEquals(resp.status_int, 200)
3735+ self.assertNotEquals(resp.headers.get('x-auth-token'), 'AUTH_tktest')
3736+ self.assertEquals(resp.headers.get('x-auth-token'),
3737+ resp.headers.get('x-storage-token'))
3738+ self.assertEquals(resp.headers.get('x-storage-url'),
3739+ 'http://127.0.0.1:8080/v1/AUTH_cfa')
3740+ self.assertEquals(json.loads(resp.body),
3741+ {"storage": {"default": "local",
3742+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
3743+ self.assertEquals(self.test_auth.app.calls, 7)
3744+
3745+ def test_get_token_success_existing_token_expired_fail_deleting_old(self):
3746+ self.test_auth.app = FakeApp(iter([
3747+ # GET of user object
3748+ ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tktest'},
3749+ json.dumps({"auth": "plaintext:key",
3750+ "groups": [{'name': "act:usr"}, {'name': "act"},
3751+ {'name': ".admin"}]})),
3752+ # GET of token
3753+ ('200 Ok', {}, json.dumps({"account": "act", "user": "usr",
3754+ "account_id": "AUTH_cfa", "groups": [{'name': "act:usr"},
3755+ {'name': "key"}, {'name': ".admin"}],
3756+ "expires": 0.0})),
3757+ # DELETE of expired token
3758+ ('503 Service Unavailable', {}, ''),
3759+ # GET of account
3760+ ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
3761+ # PUT of new token
3762+ ('201 Created', {}, ''),
3763+ # POST of token to user object
3764+ ('204 No Content', {}, ''),
3765+ # GET of services object
3766+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
3767+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
3768+ resp = Request.blank('/auth/v1.0',
3769+ headers={'X-Auth-User': 'act:usr',
3770+ 'X-Auth-Key': 'key'}).get_response(self.test_auth)
3771+ self.assertEquals(resp.status_int, 200)
3772+ self.assertNotEquals(resp.headers.get('x-auth-token'), 'AUTH_tktest')
3773+ self.assertEquals(resp.headers.get('x-auth-token'),
3774+ resp.headers.get('x-storage-token'))
3775+ self.assertEquals(resp.headers.get('x-storage-url'),
3776+ 'http://127.0.0.1:8080/v1/AUTH_cfa')
3777+ self.assertEquals(json.loads(resp.body),
3778+ {"storage": {"default": "local",
3779+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
3780+ self.assertEquals(self.test_auth.app.calls, 7)
3781+
3782+ def test_prep_success(self):
3783+ list_to_iter = [
3784+ # PUT of .auth account
3785+ ('201 Created', {}, ''),
3786+ # PUT of .account_id container
3787+ ('201 Created', {}, '')]
3788+ # PUT of .token* containers
3789+ for x in xrange(16):
3790+ list_to_iter.append(('201 Created', {}, ''))
3791+ self.test_auth.app = FakeApp(iter(list_to_iter))
3792+ resp = Request.blank('/auth/v2/.prep',
3793+ environ={'REQUEST_METHOD': 'POST'},
3794+ headers={'X-Auth-Admin-User': '.super_admin',
3795+ 'X-Auth-Admin-Key': 'supertest'}
3796+ ).get_response(self.test_auth)
3797+ self.assertEquals(resp.status_int, 204)
3798+ self.assertEquals(self.test_auth.app.calls, 18)
3799+
3800+ def test_prep_bad_method(self):
3801+ resp = Request.blank('/auth/v2/.prep',
3802+ headers={'X-Auth-Admin-User': '.super_admin',
3803+ 'X-Auth-Admin-Key': 'supertest'}
3804+ ).get_response(self.test_auth)
3805+ self.assertEquals(resp.status_int, 400)
3806+ resp = Request.blank('/auth/v2/.prep',
3807+ environ={'REQUEST_METHOD': 'HEAD'},
3808+ headers={'X-Auth-Admin-User': '.super_admin',
3809+ 'X-Auth-Admin-Key': 'supertest'}
3810+ ).get_response(self.test_auth)
3811+ self.assertEquals(resp.status_int, 400)
3812+ resp = Request.blank('/auth/v2/.prep',
3813+ environ={'REQUEST_METHOD': 'PUT'},
3814+ headers={'X-Auth-Admin-User': '.super_admin',
3815+ 'X-Auth-Admin-Key': 'supertest'}
3816+ ).get_response(self.test_auth)
3817+ self.assertEquals(resp.status_int, 400)
3818+
3819+ def test_prep_bad_creds(self):
3820+ resp = Request.blank('/auth/v2/.prep',
3821+ environ={'REQUEST_METHOD': 'POST'},
3822+ headers={'X-Auth-Admin-User': 'super_admin',
3823+ 'X-Auth-Admin-Key': 'supertest'}
3824+ ).get_response(self.test_auth)
3825+ self.assertEquals(resp.status_int, 403)
3826+ resp = Request.blank('/auth/v2/.prep',
3827+ environ={'REQUEST_METHOD': 'POST'},
3828+ headers={'X-Auth-Admin-User': '.super_admin',
3829+ 'X-Auth-Admin-Key': 'upertest'}
3830+ ).get_response(self.test_auth)
3831+ self.assertEquals(resp.status_int, 403)
3832+ resp = Request.blank('/auth/v2/.prep',
3833+ environ={'REQUEST_METHOD': 'POST'},
3834+ headers={'X-Auth-Admin-User': '.super_admin'}
3835+ ).get_response(self.test_auth)
3836+ self.assertEquals(resp.status_int, 403)
3837+ resp = Request.blank('/auth/v2/.prep',
3838+ environ={'REQUEST_METHOD': 'POST'},
3839+ headers={'X-Auth-Admin-Key': 'supertest'}
3840+ ).get_response(self.test_auth)
3841+ self.assertEquals(resp.status_int, 403)
3842+ resp = Request.blank('/auth/v2/.prep',
3843+ environ={'REQUEST_METHOD': 'POST'}).get_response(self.test_auth)
3844+ self.assertEquals(resp.status_int, 403)
3845+
3846+ def test_prep_fail_account_create(self):
3847+ self.test_auth.app = FakeApp(iter([
3848+ # PUT of .auth account
3849+ ('503 Service Unavailable', {}, '')]))
3850+ resp = Request.blank('/auth/v2/.prep',
3851+ environ={'REQUEST_METHOD': 'POST'},
3852+ headers={'X-Auth-Admin-User': '.super_admin',
3853+ 'X-Auth-Admin-Key': 'supertest'}
3854+ ).get_response(self.test_auth)
3855+ self.assertEquals(resp.status_int, 500)
3856+ self.assertEquals(self.test_auth.app.calls, 1)
3857+
3858+ def test_prep_fail_token_container_create(self):
3859+ self.test_auth.app = FakeApp(iter([
3860+ # PUT of .auth account
3861+ ('201 Created', {}, ''),
3862+ # PUT of .token container
3863+ ('503 Service Unavailable', {}, '')]))
3864+ resp = Request.blank('/auth/v2/.prep',
3865+ environ={'REQUEST_METHOD': 'POST'},
3866+ headers={'X-Auth-Admin-User': '.super_admin',
3867+ 'X-Auth-Admin-Key': 'supertest'}
3868+ ).get_response(self.test_auth)
3869+ self.assertEquals(resp.status_int, 500)
3870+ self.assertEquals(self.test_auth.app.calls, 2)
3871+
3872+ def test_prep_fail_account_id_container_create(self):
3873+ self.test_auth.app = FakeApp(iter([
3874+ # PUT of .auth account
3875+ ('201 Created', {}, ''),
3876+ # PUT of .token container
3877+ ('201 Created', {}, ''),
3878+ # PUT of .account_id container
3879+ ('503 Service Unavailable', {}, '')]))
3880+ resp = Request.blank('/auth/v2/.prep',
3881+ environ={'REQUEST_METHOD': 'POST'},
3882+ headers={'X-Auth-Admin-User': '.super_admin',
3883+ 'X-Auth-Admin-Key': 'supertest'}
3884+ ).get_response(self.test_auth)
3885+ self.assertEquals(resp.status_int, 500)
3886+ self.assertEquals(self.test_auth.app.calls, 3)
3887+
3888+ def test_get_reseller_success(self):
3889+ self.test_auth.app = FakeApp(iter([
3890+ # GET of .auth account (list containers)
3891+ ('200 Ok', {}, json.dumps([
3892+ {"name": ".token", "count": 0, "bytes": 0},
3893+ {"name": ".account_id", "count": 0, "bytes": 0},
3894+ {"name": "act", "count": 0, "bytes": 0}])),
3895+ # GET of .auth account (list containers continuation)
3896+ ('200 Ok', {}, '[]')]))
3897+ resp = Request.blank('/auth/v2',
3898+ headers={'X-Auth-Admin-User': '.super_admin',
3899+ 'X-Auth-Admin-Key': 'supertest'}
3900+ ).get_response(self.test_auth)
3901+ self.assertEquals(resp.status_int, 200)
3902+ self.assertEquals(json.loads(resp.body),
3903+ {"accounts": [{"name": "act"}]})
3904+ self.assertEquals(self.test_auth.app.calls, 2)
3905+
3906+ self.test_auth.app = FakeApp(iter([
3907+ # GET of user object
3908+ ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"},
3909+ {"name": "test"}, {"name": ".admin"},
3910+ {"name": ".reseller_admin"}], "auth": "plaintext:key"})),
3911+ # GET of .auth account (list containers)
3912+ ('200 Ok', {}, json.dumps([
3913+ {"name": ".token", "count": 0, "bytes": 0},
3914+ {"name": ".account_id", "count": 0, "bytes": 0},
3915+ {"name": "act", "count": 0, "bytes": 0}])),
3916+ # GET of .auth account (list containers continuation)
3917+ ('200 Ok', {}, '[]')]))
3918+ resp = Request.blank('/auth/v2',
3919+ headers={'X-Auth-Admin-User': 'act:adm',
3920+ 'X-Auth-Admin-Key': 'key'}
3921+ ).get_response(self.test_auth)
3922+ self.assertEquals(resp.status_int, 200)
3923+ self.assertEquals(json.loads(resp.body),
3924+ {"accounts": [{"name": "act"}]})
3925+ self.assertEquals(self.test_auth.app.calls, 3)
3926+
3927+ def test_get_reseller_fail_bad_creds(self):
3928+ self.test_auth.app = FakeApp(iter([
3929+ # GET of user object
3930+ ('404 Not Found', {}, '')]))
3931+ resp = Request.blank('/auth/v2',
3932+ headers={'X-Auth-Admin-User': 'super:admin',
3933+ 'X-Auth-Admin-Key': 'supertest'}
3934+ ).get_response(self.test_auth)
3935+ self.assertEquals(resp.status_int, 403)
3936+ self.assertEquals(self.test_auth.app.calls, 1)
3937+
3938+ self.test_auth.app = FakeApp(iter([
3939+ # GET of user object (account admin, but not reseller admin)
3940+ ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"},
3941+ {"name": "test"}, {"name": ".admin"}],
3942+ "auth": "plaintext:key"}))]))
3943+ resp = Request.blank('/auth/v2',
3944+ headers={'X-Auth-Admin-User': 'act:adm',
3945+ 'X-Auth-Admin-Key': 'key'}
3946+ ).get_response(self.test_auth)
3947+ self.assertEquals(resp.status_int, 403)
3948+ self.assertEquals(self.test_auth.app.calls, 1)
3949+
3950+ self.test_auth.app = FakeApp(iter([
3951+ # GET of user object (regular user)
3952+ ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"},
3953+ {"name": "test"}], "auth": "plaintext:key"}))]))
3954+ resp = Request.blank('/auth/v2',
3955+ headers={'X-Auth-Admin-User': 'act:usr',
3956+ 'X-Auth-Admin-Key': 'key'}
3957+ ).get_response(self.test_auth)
3958+ self.assertEquals(resp.status_int, 403)
3959+ self.assertEquals(self.test_auth.app.calls, 1)
3960+
3961+ def test_get_reseller_fail_listing(self):
3962+ self.test_auth.app = FakeApp(iter([
3963+ # GET of .auth account (list containers)
3964+ ('503 Service Unavailable', {}, '')]))
3965+ resp = Request.blank('/auth/v2',
3966+ headers={'X-Auth-Admin-User': '.super_admin',
3967+ 'X-Auth-Admin-Key': 'supertest'}
3968+ ).get_response(self.test_auth)
3969+ self.assertEquals(resp.status_int, 500)
3970+ self.assertEquals(self.test_auth.app.calls, 1)
3971+
3972+ self.test_auth.app = FakeApp(iter([
3973+ # GET of .auth account (list containers)
3974+ ('200 Ok', {}, json.dumps([
3975+ {"name": ".token", "count": 0, "bytes": 0},
3976+ {"name": ".account_id", "count": 0, "bytes": 0},
3977+ {"name": "act", "count": 0, "bytes": 0}])),
3978+ # GET of .auth account (list containers continuation)
3979+ ('503 Service Unavailable', {}, '')]))
3980+ resp = Request.blank('/auth/v2',
3981+ headers={'X-Auth-Admin-User': '.super_admin',
3982+ 'X-Auth-Admin-Key': 'supertest'}
3983+ ).get_response(self.test_auth)
3984+ self.assertEquals(resp.status_int, 500)
3985+ self.assertEquals(self.test_auth.app.calls, 2)
3986+
3987+ def test_get_account_success(self):
3988+ self.test_auth.app = FakeApp(iter([
3989+ # GET of .services object
3990+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
3991+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
3992+ # GET of account container (list objects)
3993+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
3994+ json.dumps([
3995+ {"name": ".services", "hash": "etag", "bytes": 112,
3996+ "content_type": "application/octet-stream",
3997+ "last_modified": "2010-12-03T17:16:27.618110"},
3998+ {"name": "tester", "hash": "etag", "bytes": 104,
3999+ "content_type": "application/octet-stream",
4000+ "last_modified": "2010-12-03T17:16:27.736680"},
4001+ {"name": "tester3", "hash": "etag", "bytes": 86,
4002+ "content_type": "application/octet-stream",
4003+ "last_modified": "2010-12-03T17:16:28.135530"}])),
4004+ # GET of account container (list objects continuation)
4005+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]')]))
4006+ resp = Request.blank('/auth/v2/act',
4007+ headers={'X-Auth-Admin-User': '.super_admin',
4008+ 'X-Auth-Admin-Key': 'supertest'}
4009+ ).get_response(self.test_auth)
4010+ self.assertEquals(resp.status_int, 200)
4011+ self.assertEquals(json.loads(resp.body),
4012+ {'account_id': 'AUTH_cfa',
4013+ 'services': {'storage':
4014+ {'default': 'local',
4015+ 'local': 'http://127.0.0.1:8080/v1/AUTH_cfa'}},
4016+ 'users': [{'name': 'tester'}, {'name': 'tester3'}]})
4017+ self.assertEquals(self.test_auth.app.calls, 3)
4018+
4019+ self.test_auth.app = FakeApp(iter([
4020+ # GET of user object
4021+ ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"},
4022+ {"name": "test"}, {"name": ".admin"}],
4023+ "auth": "plaintext:key"})),
4024+ # GET of .services object
4025+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
4026+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
4027+ # GET of account container (list objects)
4028+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
4029+ json.dumps([
4030+ {"name": ".services", "hash": "etag", "bytes": 112,
4031+ "content_type": "application/octet-stream",
4032+ "last_modified": "2010-12-03T17:16:27.618110"},
4033+ {"name": "tester", "hash": "etag", "bytes": 104,
4034+ "content_type": "application/octet-stream",
4035+ "last_modified": "2010-12-03T17:16:27.736680"},
4036+ {"name": "tester3", "hash": "etag", "bytes": 86,
4037+ "content_type": "application/octet-stream",
4038+ "last_modified": "2010-12-03T17:16:28.135530"}])),
4039+ # GET of account container (list objects continuation)
4040+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]')]))
4041+ resp = Request.blank('/auth/v2/act',
4042+ headers={'X-Auth-Admin-User': 'act:adm',
4043+ 'X-Auth-Admin-Key': 'key'}
4044+ ).get_response(self.test_auth)
4045+ self.assertEquals(resp.status_int, 200)
4046+ self.assertEquals(json.loads(resp.body),
4047+ {'account_id': 'AUTH_cfa',
4048+ 'services': {'storage':
4049+ {'default': 'local',
4050+ 'local': 'http://127.0.0.1:8080/v1/AUTH_cfa'}},
4051+ 'users': [{'name': 'tester'}, {'name': 'tester3'}]})
4052+ self.assertEquals(self.test_auth.app.calls, 4)
4053+
4054+ def test_get_account_fail_bad_account_name(self):
4055+ resp = Request.blank('/auth/v2/.token',
4056+ headers={'X-Auth-Admin-User': '.super_admin',
4057+ 'X-Auth-Admin-Key': 'supertest'}
4058+ ).get_response(self.test_auth)
4059+ self.assertEquals(resp.status_int, 400)
4060+ resp = Request.blank('/auth/v2/.anything',
4061+ headers={'X-Auth-Admin-User': '.super_admin',
4062+ 'X-Auth-Admin-Key': 'supertest'}
4063+ ).get_response(self.test_auth)
4064+ self.assertEquals(resp.status_int, 400)
4065+
4066+ def test_get_account_fail_creds(self):
4067+ self.test_auth.app = FakeApp(iter([
4068+ # GET of user object
4069+ ('404 Not Found', {}, '')]))
4070+ resp = Request.blank('/auth/v2/act',
4071+ headers={'X-Auth-Admin-User': 'super:admin',
4072+ 'X-Auth-Admin-Key': 'supertest'}
4073+ ).get_response(self.test_auth)
4074+ self.assertEquals(resp.status_int, 403)
4075+ self.assertEquals(self.test_auth.app.calls, 1)
4076+
4077+ self.test_auth.app = FakeApp(iter([
4078+ # GET of user object (account admin, but wrong account)
4079+ ('200 Ok', {}, json.dumps({"groups": [{"name": "act2:adm"},
4080+ {"name": "test"}, {"name": ".admin"}],
4081+ "auth": "plaintext:key"}))]))
4082+ resp = Request.blank('/auth/v2/act',
4083+ headers={'X-Auth-Admin-User': 'act2:adm',
4084+ 'X-Auth-Admin-Key': 'key'}
4085+ ).get_response(self.test_auth)
4086+ self.assertEquals(resp.status_int, 403)
4087+ self.assertEquals(self.test_auth.app.calls, 1)
4088+
4089+ self.test_auth.app = FakeApp(iter([
4090+ # GET of user object (regular user)
4091+ ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"},
4092+ {"name": "test"}], "auth": "plaintext:key"}))]))
4093+ resp = Request.blank('/auth/v2/act',
4094+ headers={'X-Auth-Admin-User': 'act:usr',
4095+ 'X-Auth-Admin-Key': 'key'}
4096+ ).get_response(self.test_auth)
4097+ self.assertEquals(resp.status_int, 403)
4098+ self.assertEquals(self.test_auth.app.calls, 1)
4099+
4100+ def test_get_account_fail_get_services(self):
4101+ self.test_auth.app = FakeApp(iter([
4102+ # GET of .services object
4103+ ('503 Service Unavailable', {}, '')]))
4104+ resp = Request.blank('/auth/v2/act',
4105+ headers={'X-Auth-Admin-User': '.super_admin',
4106+ 'X-Auth-Admin-Key': 'supertest'}
4107+ ).get_response(self.test_auth)
4108+ self.assertEquals(resp.status_int, 500)
4109+ self.assertEquals(self.test_auth.app.calls, 1)
4110+
4111+ self.test_auth.app = FakeApp(iter([
4112+ # GET of .services object
4113+ ('404 Not Found', {}, '')]))
4114+ resp = Request.blank('/auth/v2/act',
4115+ headers={'X-Auth-Admin-User': '.super_admin',
4116+ 'X-Auth-Admin-Key': 'supertest'}
4117+ ).get_response(self.test_auth)
4118+ self.assertEquals(resp.status_int, 404)
4119+ self.assertEquals(self.test_auth.app.calls, 1)
4120+
4121+ def test_get_account_fail_listing(self):
4122+ self.test_auth.app = FakeApp(iter([
4123+ # GET of .services object
4124+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
4125+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
4126+ # GET of account container (list objects)
4127+ ('503 Service Unavailable', {}, '')]))
4128+ resp = Request.blank('/auth/v2/act',
4129+ headers={'X-Auth-Admin-User': '.super_admin',
4130+ 'X-Auth-Admin-Key': 'supertest'}
4131+ ).get_response(self.test_auth)
4132+ self.assertEquals(resp.status_int, 500)
4133+ self.assertEquals(self.test_auth.app.calls, 2)
4134+
4135+ self.test_auth.app = FakeApp(iter([
4136+ # GET of .services object
4137+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
4138+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
4139+ # GET of account container (list objects)
4140+ ('404 Not Found', {}, '')]))
4141+ resp = Request.blank('/auth/v2/act',
4142+ headers={'X-Auth-Admin-User': '.super_admin',
4143+ 'X-Auth-Admin-Key': 'supertest'}
4144+ ).get_response(self.test_auth)
4145+ self.assertEquals(resp.status_int, 404)
4146+ self.assertEquals(self.test_auth.app.calls, 2)
4147+
4148+ self.test_auth.app = FakeApp(iter([
4149+ # GET of .services object
4150+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
4151+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
4152+ # GET of account container (list objects)
4153+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
4154+ json.dumps([
4155+ {"name": ".services", "hash": "etag", "bytes": 112,
4156+ "content_type": "application/octet-stream",
4157+ "last_modified": "2010-12-03T17:16:27.618110"},
4158+ {"name": "tester", "hash": "etag", "bytes": 104,
4159+ "content_type": "application/octet-stream",
4160+ "last_modified": "2010-12-03T17:16:27.736680"},
4161+ {"name": "tester3", "hash": "etag", "bytes": 86,
4162+ "content_type": "application/octet-stream",
4163+ "last_modified": "2010-12-03T17:16:28.135530"}])),
4164+ # GET of account container (list objects continuation)
4165+ ('503 Service Unavailable', {}, '')]))
4166+ resp = Request.blank('/auth/v2/act',
4167+ headers={'X-Auth-Admin-User': '.super_admin',
4168+ 'X-Auth-Admin-Key': 'supertest'}
4169+ ).get_response(self.test_auth)
4170+ self.assertEquals(resp.status_int, 500)
4171+ self.assertEquals(self.test_auth.app.calls, 3)
4172+
4173+ def test_set_services_new_service(self):
4174+ self.test_auth.app = FakeApp(iter([
4175+ # GET of .services object
4176+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
4177+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
4178+ # PUT of new .services object
4179+ ('204 No Content', {}, '')]))
4180+ resp = Request.blank('/auth/v2/act/.services',
4181+ environ={'REQUEST_METHOD': 'POST'},
4182+ headers={'X-Auth-Admin-User': '.super_admin',
4183+ 'X-Auth-Admin-Key': 'supertest'},
4184+ body=json.dumps({'new_service': {'new_endpoint': 'new_value'}})
4185+ ).get_response(self.test_auth)
4186+ self.assertEquals(resp.status_int, 200)
4187+ self.assertEquals(json.loads(resp.body),
4188+ {'storage': {'default': 'local',
4189+ 'local': 'http://127.0.0.1:8080/v1/AUTH_cfa'},
4190+ 'new_service': {'new_endpoint': 'new_value'}})
4191+ self.assertEquals(self.test_auth.app.calls, 2)
4192+
4193+ def test_set_services_new_endpoint(self):
4194+ self.test_auth.app = FakeApp(iter([
4195+ # GET of .services object
4196+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
4197+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
4198+ # PUT of new .services object
4199+ ('204 No Content', {}, '')]))
4200+ resp = Request.blank('/auth/v2/act/.services',
4201+ environ={'REQUEST_METHOD': 'POST'},
4202+ headers={'X-Auth-Admin-User': '.super_admin',
4203+ 'X-Auth-Admin-Key': 'supertest'},
4204+ body=json.dumps({'storage': {'new_endpoint': 'new_value'}})
4205+ ).get_response(self.test_auth)
4206+ self.assertEquals(resp.status_int, 200)
4207+ self.assertEquals(json.loads(resp.body),
4208+ {'storage': {'default': 'local',
4209+ 'local': 'http://127.0.0.1:8080/v1/AUTH_cfa',
4210+ 'new_endpoint': 'new_value'}})
4211+ self.assertEquals(self.test_auth.app.calls, 2)
4212+
4213+ def test_set_services_update_endpoint(self):
4214+ self.test_auth.app = FakeApp(iter([
4215+ # GET of .services object
4216+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
4217+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
4218+ # PUT of new .services object
4219+ ('204 No Content', {}, '')]))
4220+ resp = Request.blank('/auth/v2/act/.services',
4221+ environ={'REQUEST_METHOD': 'POST'},
4222+ headers={'X-Auth-Admin-User': '.super_admin',
4223+ 'X-Auth-Admin-Key': 'supertest'},
4224+ body=json.dumps({'storage': {'local': 'new_value'}})
4225+ ).get_response(self.test_auth)
4226+ self.assertEquals(resp.status_int, 200)
4227+ self.assertEquals(json.loads(resp.body),
4228+ {'storage': {'default': 'local',
4229+ 'local': 'new_value'}})
4230+ self.assertEquals(self.test_auth.app.calls, 2)
4231+
4232+ def test_set_services_fail_bad_creds(self):
4233+ self.test_auth.app = FakeApp(iter([
4234+ # GET of user object
4235+ ('404 Not Found', {}, '')]))
4236+ resp = Request.blank('/auth/v2/act/.services',
4237+ environ={'REQUEST_METHOD': 'POST'},
4238+ headers={'X-Auth-Admin-User': 'super:admin',
4239+ 'X-Auth-Admin-Key': 'supertest'},
4240+ body=json.dumps({'storage': {'local': 'new_value'}})
4241+ ).get_response(self.test_auth)
4242+ self.assertEquals(resp.status_int, 403)
4243+ self.assertEquals(self.test_auth.app.calls, 1)
4244+
4245+ self.test_auth.app = FakeApp(iter([
4246+ # GET of user object (account admin, but not reseller admin)
4247+ ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"},
4248+ {"name": "test"}, {"name": ".admin"}],
4249+ "auth": "plaintext:key"}))]))
4250+ resp = Request.blank('/auth/v2/act/.services',
4251+ environ={'REQUEST_METHOD': 'POST'},
4252+ headers={'X-Auth-Admin-User': 'act:adm',
4253+ 'X-Auth-Admin-Key': 'key'},
4254+ body=json.dumps({'storage': {'local': 'new_value'}})
4255+ ).get_response(self.test_auth)
4256+ self.assertEquals(resp.status_int, 403)
4257+ self.assertEquals(self.test_auth.app.calls, 1)
4258+
4259+ self.test_auth.app = FakeApp(iter([
4260+ # GET of user object (regular user)
4261+ ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"},
4262+ {"name": "test"}], "auth": "plaintext:key"}))]))
4263+ resp = Request.blank('/auth/v2/act/.services',
4264+ environ={'REQUEST_METHOD': 'POST'},
4265+ headers={'X-Auth-Admin-User': 'act:usr',
4266+ 'X-Auth-Admin-Key': 'key'},
4267+ body=json.dumps({'storage': {'local': 'new_value'}})
4268+ ).get_response(self.test_auth)
4269+ self.assertEquals(resp.status_int, 403)
4270+ self.assertEquals(self.test_auth.app.calls, 1)
4271+
4272+ def test_set_services_fail_bad_account_name(self):
4273+ resp = Request.blank('/auth/v2/.act/.services',
4274+ environ={'REQUEST_METHOD': 'POST'},
4275+ headers={'X-Auth-Admin-User': '.super_admin',
4276+ 'X-Auth-Admin-Key': 'supertest'},
4277+ body=json.dumps({'storage': {'local': 'new_value'}})
4278+ ).get_response(self.test_auth)
4279+ self.assertEquals(resp.status_int, 400)
4280+
4281+ def test_set_services_fail_bad_json(self):
4282+ resp = Request.blank('/auth/v2/act/.services',
4283+ environ={'REQUEST_METHOD': 'POST'},
4284+ headers={'X-Auth-Admin-User': '.super_admin',
4285+ 'X-Auth-Admin-Key': 'supertest'},
4286+ body='garbage'
4287+ ).get_response(self.test_auth)
4288+ self.assertEquals(resp.status_int, 400)
4289+ resp = Request.blank('/auth/v2/act/.services',
4290+ environ={'REQUEST_METHOD': 'POST'},
4291+ headers={'X-Auth-Admin-User': '.super_admin',
4292+ 'X-Auth-Admin-Key': 'supertest'},
4293+ body=''
4294+ ).get_response(self.test_auth)
4295+ self.assertEquals(resp.status_int, 400)
4296+
4297+ def test_set_services_fail_get_services(self):
4298+ self.test_auth.app = FakeApp(iter([
4299+ # GET of .services object
4300+ ('503 Unavailable', {}, '')]))
4301+ resp = Request.blank('/auth/v2/act/.services',
4302+ environ={'REQUEST_METHOD': 'POST'},
4303+ headers={'X-Auth-Admin-User': '.super_admin',
4304+ 'X-Auth-Admin-Key': 'supertest'},
4305+ body=json.dumps({'new_service': {'new_endpoint': 'new_value'}})
4306+ ).get_response(self.test_auth)
4307+ self.assertEquals(resp.status_int, 500)
4308+ self.assertEquals(self.test_auth.app.calls, 1)
4309+
4310+ self.test_auth.app = FakeApp(iter([
4311+ # GET of .services object
4312+ ('404 Not Found', {}, '')]))
4313+ resp = Request.blank('/auth/v2/act/.services',
4314+ environ={'REQUEST_METHOD': 'POST'},
4315+ headers={'X-Auth-Admin-User': '.super_admin',
4316+ 'X-Auth-Admin-Key': 'supertest'},
4317+ body=json.dumps({'new_service': {'new_endpoint': 'new_value'}})
4318+ ).get_response(self.test_auth)
4319+ self.assertEquals(resp.status_int, 404)
4320+ self.assertEquals(self.test_auth.app.calls, 1)
4321+
4322+ def test_set_services_fail_put_services(self):
4323+ self.test_auth.app = FakeApp(iter([
4324+ # GET of .services object
4325+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
4326+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
4327+ # PUT of new .services object
4328+ ('503 Unavailable', {}, '')]))
4329+ resp = Request.blank('/auth/v2/act/.services',
4330+ environ={'REQUEST_METHOD': 'POST'},
4331+ headers={'X-Auth-Admin-User': '.super_admin',
4332+ 'X-Auth-Admin-Key': 'supertest'},
4333+ body=json.dumps({'new_service': {'new_endpoint': 'new_value'}})
4334+ ).get_response(self.test_auth)
4335+ self.assertEquals(resp.status_int, 500)
4336+ self.assertEquals(self.test_auth.app.calls, 2)
4337+
4338+ def test_put_account_success(self):
4339+ conn = FakeConn(iter([
4340+ # PUT of storage account itself
4341+ ('201 Created', {}, '')]))
4342+ self.test_auth.get_conn = lambda: conn
4343+ self.test_auth.app = FakeApp(iter([
4344+ # Initial HEAD of account container to check for pre-existence
4345+ ('404 Not Found', {}, ''),
4346+ # PUT of account container
4347+ ('204 No Content', {}, ''),
4348+ # PUT of .account_id mapping object
4349+ ('204 No Content', {}, ''),
4350+ # PUT of .services object
4351+ ('204 No Content', {}, ''),
4352+ # POST to account container updating X-Container-Meta-Account-Id
4353+ ('204 No Content', {}, '')]))
4354+ resp = Request.blank('/auth/v2/act',
4355+ environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
4356+ headers={'X-Auth-Admin-User': '.super_admin',
4357+ 'X-Auth-Admin-Key': 'supertest'}
4358+ ).get_response(self.test_auth)
4359+ self.assertEquals(resp.status_int, 201)
4360+ self.assertEquals(self.test_auth.app.calls, 5)
4361+ self.assertEquals(conn.calls, 1)
4362+
4363+ def test_put_account_success_preexist_but_not_completed(self):
4364+ conn = FakeConn(iter([
4365+ # PUT of storage account itself
4366+ ('201 Created', {}, '')]))
4367+ self.test_auth.get_conn = lambda: conn
4368+ self.test_auth.app = FakeApp(iter([
4369+ # Initial HEAD of account container to check for pre-existence
4370+ # We're going to show it as existing this time, but with no
4371+ # X-Container-Meta-Account-Id, indicating a failed previous attempt
4372+ ('200 Ok', {}, ''),
4373+ # PUT of .account_id mapping object
4374+ ('204 No Content', {}, ''),
4375+ # PUT of .services object
4376+ ('204 No Content', {}, ''),
4377+ # POST to account container updating X-Container-Meta-Account-Id
4378+ ('204 No Content', {}, '')]))
4379+ resp = Request.blank('/auth/v2/act',
4380+ environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
4381+ headers={'X-Auth-Admin-User': '.super_admin',
4382+ 'X-Auth-Admin-Key': 'supertest'}
4383+ ).get_response(self.test_auth)
4384+ self.assertEquals(resp.status_int, 201)
4385+ self.assertEquals(self.test_auth.app.calls, 4)
4386+ self.assertEquals(conn.calls, 1)
4387+
4388+ def test_put_account_success_preexist_and_completed(self):
4389+ self.test_auth.app = FakeApp(iter([
4390+ # Initial HEAD of account container to check for pre-existence
4391+ # We're going to show it as existing this time, and with an
4392+ # X-Container-Meta-Account-Id, indicating it already exists
4393+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '')]))
4394+ resp = Request.blank('/auth/v2/act',
4395+ environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
4396+ headers={'X-Auth-Admin-User': '.super_admin',
4397+ 'X-Auth-Admin-Key': 'supertest'}
4398+ ).get_response(self.test_auth)
4399+ self.assertEquals(resp.status_int, 202)
4400+ self.assertEquals(self.test_auth.app.calls, 1)
4401+
4402+ def test_put_account_success_with_given_suffix(self):
4403+ conn = FakeConn(iter([
4404+ # PUT of storage account itself
4405+ ('201 Created', {}, '')]))
4406+ self.test_auth.get_conn = lambda: conn
4407+ self.test_auth.app = FakeApp(iter([
4408+ # Initial HEAD of account container to check for pre-existence
4409+ ('404 Not Found', {}, ''),
4410+ # PUT of account container
4411+ ('204 No Content', {}, ''),
4412+ # PUT of .account_id mapping object
4413+ ('204 No Content', {}, ''),
4414+ # PUT of .services object
4415+ ('204 No Content', {}, ''),
4416+ # POST to account container updating X-Container-Meta-Account-Id
4417+ ('204 No Content', {}, '')]))
4418+ resp = Request.blank('/auth/v2/act',
4419+ environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
4420+ headers={'X-Auth-Admin-User': '.super_admin',
4421+ 'X-Auth-Admin-Key': 'supertest',
4422+ 'X-Account-Suffix': 'test-suffix'}
4423+ ).get_response(self.test_auth)
4424+ self.assertEquals(resp.status_int, 201)
4425+ self.assertEquals(conn.request_path, '/v1/AUTH_test-suffix')
4426+ self.assertEquals(self.test_auth.app.calls, 5)
4427+ self.assertEquals(conn.calls, 1)
4428+
4429+ def test_put_account_fail_bad_creds(self):
4430+ self.test_auth.app = FakeApp(iter([
4431+ # GET of user object
4432+ ('404 Not Found', {}, '')]))
4433+ resp = Request.blank('/auth/v2/act',
4434+ environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
4435+ headers={'X-Auth-Admin-User': 'super:admin',
4436+ 'X-Auth-Admin-Key': 'supertest'},
4437+ ).get_response(self.test_auth)
4438+ self.assertEquals(resp.status_int, 403)
4439+ self.assertEquals(self.test_auth.app.calls, 1)
4440+
4441+ self.test_auth.app = FakeApp(iter([
4442+ # GET of user object (account admin, but not reseller admin)
4443+ ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"},
4444+ {"name": "test"}, {"name": ".admin"}],
4445+ "auth": "plaintext:key"}))]))
4446+ resp = Request.blank('/auth/v2/act',
4447+ environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
4448+ headers={'X-Auth-Admin-User': 'act:adm',
4449+ 'X-Auth-Admin-Key': 'key'},
4450+ ).get_response(self.test_auth)
4451+ self.assertEquals(resp.status_int, 403)
4452+ self.assertEquals(self.test_auth.app.calls, 1)
4453+
4454+ self.test_auth.app = FakeApp(iter([
4455+ # GET of user object (regular user)
4456+ ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"},
4457+ {"name": "test"}], "auth": "plaintext:key"}))]))
4458+ resp = Request.blank('/auth/v2/act',
4459+ environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
4460+ headers={'X-Auth-Admin-User': 'act:usr',
4461+ 'X-Auth-Admin-Key': 'key'},
4462+ ).get_response(self.test_auth)
4463+ self.assertEquals(resp.status_int, 403)
4464+ self.assertEquals(self.test_auth.app.calls, 1)
4465+
4466+ def test_put_account_fail_invalid_account_name(self):
4467+ resp = Request.blank('/auth/v2/.act',
4468+ environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
4469+ headers={'X-Auth-Admin-User': '.super_admin',
4470+ 'X-Auth-Admin-Key': 'supertest'},
4471+ ).get_response(self.test_auth)
4472+ self.assertEquals(resp.status_int, 400)
4473+
4474+ def test_put_account_fail_on_initial_account_head(self):
4475+ self.test_auth.app = FakeApp(iter([
4476+ # Initial HEAD of account container to check for pre-existence
4477+ ('503 Service Unavailable', {}, '')]))
4478+ resp = Request.blank('/auth/v2/act',
4479+ environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
4480+ headers={'X-Auth-Admin-User': '.super_admin',
4481+ 'X-Auth-Admin-Key': 'supertest'}
4482+ ).get_response(self.test_auth)
4483+ self.assertEquals(resp.status_int, 500)
4484+ self.assertEquals(self.test_auth.app.calls, 1)
4485+
4486+ def test_put_account_fail_on_account_marker_put(self):
4487+ self.test_auth.app = FakeApp(iter([
4488+ # Initial HEAD of account container to check for pre-existence
4489+ ('404 Not Found', {}, ''),
4490+ # PUT of account container
4491+ ('503 Service Unavailable', {}, '')]))
4492+ resp = Request.blank('/auth/v2/act',
4493+ environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
4494+ headers={'X-Auth-Admin-User': '.super_admin',
4495+ 'X-Auth-Admin-Key': 'supertest'}
4496+ ).get_response(self.test_auth)
4497+ self.assertEquals(resp.status_int, 500)
4498+ self.assertEquals(self.test_auth.app.calls, 2)
4499+
4500+ def test_put_account_fail_on_storage_account_put(self):
4501+ conn = FakeConn(iter([
4502+ # PUT of storage account itself
4503+ ('503 Service Unavailable', {}, '')]))
4504+ self.test_auth.get_conn = lambda: conn
4505+ self.test_auth.app = FakeApp(iter([
4506+ # Initial HEAD of account container to check for pre-existence
4507+ ('404 Not Found', {}, ''),
4508+ # PUT of account container
4509+ ('204 No Content', {}, '')]))
4510+ resp = Request.blank('/auth/v2/act',
4511+ environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
4512+ headers={'X-Auth-Admin-User': '.super_admin',
4513+ 'X-Auth-Admin-Key': 'supertest'}
4514+ ).get_response(self.test_auth)
4515+ self.assertEquals(resp.status_int, 500)
4516+ self.assertEquals(conn.calls, 1)
4517+ self.assertEquals(self.test_auth.app.calls, 2)
4518+
4519+ def test_put_account_fail_on_account_id_mapping(self):
4520+ conn = FakeConn(iter([
4521+ # PUT of storage account itself
4522+ ('201 Created', {}, '')]))
4523+ self.test_auth.get_conn = lambda: conn
4524+ self.test_auth.app = FakeApp(iter([
4525+ # Initial HEAD of account container to check for pre-existence
4526+ ('404 Not Found', {}, ''),
4527+ # PUT of account container
4528+ ('204 No Content', {}, ''),
4529+ # PUT of .account_id mapping object
4530+ ('503 Service Unavailable', {}, '')]))
4531+ resp = Request.blank('/auth/v2/act',
4532+ environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
4533+ headers={'X-Auth-Admin-User': '.super_admin',
4534+ 'X-Auth-Admin-Key': 'supertest'}
4535+ ).get_response(self.test_auth)
4536+ self.assertEquals(resp.status_int, 500)
4537+ self.assertEquals(conn.calls, 1)
4538+ self.assertEquals(self.test_auth.app.calls, 3)
4539+
4540+ def test_put_account_fail_on_services_object(self):
4541+ conn = FakeConn(iter([
4542+ # PUT of storage account itself
4543+ ('201 Created', {}, '')]))
4544+ self.test_auth.get_conn = lambda: conn
4545+ self.test_auth.app = FakeApp(iter([
4546+ # Initial HEAD of account container to check for pre-existence
4547+ ('404 Not Found', {}, ''),
4548+ # PUT of account container
4549+ ('204 No Content', {}, ''),
4550+ # PUT of .account_id mapping object
4551+ ('204 No Content', {}, ''),
4552+ # PUT of .services object
4553+ ('503 Service Unavailable', {}, '')]))
4554+ resp = Request.blank('/auth/v2/act',
4555+ environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
4556+ headers={'X-Auth-Admin-User': '.super_admin',
4557+ 'X-Auth-Admin-Key': 'supertest'}
4558+ ).get_response(self.test_auth)
4559+ self.assertEquals(resp.status_int, 500)
4560+ self.assertEquals(conn.calls, 1)
4561+ self.assertEquals(self.test_auth.app.calls, 4)
4562+
4563+ def test_put_account_fail_on_post_mapping(self):
4564+ conn = FakeConn(iter([
4565+ # PUT of storage account itself
4566+ ('201 Created', {}, '')]))
4567+ self.test_auth.get_conn = lambda: conn
4568+ self.test_auth.app = FakeApp(iter([
4569+ # Initial HEAD of account container to check for pre-existence
4570+ ('404 Not Found', {}, ''),
4571+ # PUT of account container
4572+ ('204 No Content', {}, ''),
4573+ # PUT of .account_id mapping object
4574+ ('204 No Content', {}, ''),
4575+ # PUT of .services object
4576+ ('204 No Content', {}, ''),
4577+ # POST to account container updating X-Container-Meta-Account-Id
4578+ ('503 Service Unavailable', {}, '')]))
4579+ resp = Request.blank('/auth/v2/act',
4580+ environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
4581+ headers={'X-Auth-Admin-User': '.super_admin',
4582+ 'X-Auth-Admin-Key': 'supertest'}
4583+ ).get_response(self.test_auth)
4584+ self.assertEquals(resp.status_int, 500)
4585+ self.assertEquals(conn.calls, 1)
4586+ self.assertEquals(self.test_auth.app.calls, 5)
4587+
4588+ def test_delete_account_success(self):
4589+ conn = FakeConn(iter([
4590+ # DELETE of storage account itself
4591+ ('204 No Content', {}, '')]))
4592+ self.test_auth.get_conn = lambda x: conn
4593+ self.test_auth.app = FakeApp(iter([
4594+ # Account's container listing, checking for users
4595+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
4596+ json.dumps([
4597+ {"name": ".services", "hash": "etag", "bytes": 112,
4598+ "content_type": "application/octet-stream",
4599+ "last_modified": "2010-12-03T17:16:27.618110"}])),
4600+ # Account's container listing, checking for users (continuation)
4601+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'),
4602+ # GET the .services object
4603+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
4604+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
4605+ # DELETE the .services object
4606+ ('204 No Content', {}, ''),
4607+ # DELETE the .account_id mapping object
4608+ ('204 No Content', {}, ''),
4609+ # DELETE the account container
4610+ ('204 No Content', {}, '')]))
4611+ resp = Request.blank('/auth/v2/act',
4612+ environ={'REQUEST_METHOD': 'DELETE',
4613+ 'swift.cache': FakeMemcache()},
4614+ headers={'X-Auth-Admin-User': '.super_admin',
4615+ 'X-Auth-Admin-Key': 'supertest'}
4616+ ).get_response(self.test_auth)
4617+ self.assertEquals(resp.status_int, 204)
4618+ self.assertEquals(self.test_auth.app.calls, 6)
4619+ self.assertEquals(conn.calls, 1)
4620+
4621+ def test_delete_account_success_missing_services(self):
4622+ self.test_auth.app = FakeApp(iter([
4623+ # Account's container listing, checking for users
4624+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
4625+ json.dumps([
4626+ {"name": ".services", "hash": "etag", "bytes": 112,
4627+ "content_type": "application/octet-stream",
4628+ "last_modified": "2010-12-03T17:16:27.618110"}])),
4629+ # Account's container listing, checking for users (continuation)
4630+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'),
4631+ # GET the .services object
4632+ ('404 Not Found', {}, ''),
4633+ # DELETE the .account_id mapping object
4634+ ('204 No Content', {}, ''),
4635+ # DELETE the account container
4636+ ('204 No Content', {}, '')]))
4637+ resp = Request.blank('/auth/v2/act',
4638+ environ={'REQUEST_METHOD': 'DELETE',
4639+ 'swift.cache': FakeMemcache()},
4640+ headers={'X-Auth-Admin-User': '.super_admin',
4641+ 'X-Auth-Admin-Key': 'supertest'}
4642+ ).get_response(self.test_auth)
4643+ self.assertEquals(resp.status_int, 204)
4644+ self.assertEquals(self.test_auth.app.calls, 5)
4645+
4646+ def test_delete_account_success_missing_storage_account(self):
4647+ conn = FakeConn(iter([
4648+ # DELETE of storage account itself
4649+ ('404 Not Found', {}, '')]))
4650+ self.test_auth.get_conn = lambda x: conn
4651+ self.test_auth.app = FakeApp(iter([
4652+ # Account's container listing, checking for users
4653+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
4654+ json.dumps([
4655+ {"name": ".services", "hash": "etag", "bytes": 112,
4656+ "content_type": "application/octet-stream",
4657+ "last_modified": "2010-12-03T17:16:27.618110"}])),
4658+ # Account's container listing, checking for users (continuation)
4659+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'),
4660+ # GET the .services object
4661+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
4662+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
4663+ # DELETE the .services object
4664+ ('204 No Content', {}, ''),
4665+ # DELETE the .account_id mapping object
4666+ ('204 No Content', {}, ''),
4667+ # DELETE the account container
4668+ ('204 No Content', {}, '')]))
4669+ resp = Request.blank('/auth/v2/act',
4670+ environ={'REQUEST_METHOD': 'DELETE',
4671+ 'swift.cache': FakeMemcache()},
4672+ headers={'X-Auth-Admin-User': '.super_admin',
4673+ 'X-Auth-Admin-Key': 'supertest'}
4674+ ).get_response(self.test_auth)
4675+ self.assertEquals(resp.status_int, 204)
4676+ self.assertEquals(self.test_auth.app.calls, 6)
4677+ self.assertEquals(conn.calls, 1)
4678+
4679+ def test_delete_account_success_missing_account_id_mapping(self):
4680+ conn = FakeConn(iter([
4681+ # DELETE of storage account itself
4682+ ('204 No Content', {}, '')]))
4683+ self.test_auth.get_conn = lambda x: conn
4684+ self.test_auth.app = FakeApp(iter([
4685+ # Account's container listing, checking for users
4686+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
4687+ json.dumps([
4688+ {"name": ".services", "hash": "etag", "bytes": 112,
4689+ "content_type": "application/octet-stream",
4690+ "last_modified": "2010-12-03T17:16:27.618110"}])),
4691+ # Account's container listing, checking for users (continuation)
4692+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'),
4693+ # GET the .services object
4694+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
4695+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
4696+ # DELETE the .services object
4697+ ('204 No Content', {}, ''),
4698+ # DELETE the .account_id mapping object
4699+ ('404 Not Found', {}, ''),
4700+ # DELETE the account container
4701+ ('204 No Content', {}, '')]))
4702+ resp = Request.blank('/auth/v2/act',
4703+ environ={'REQUEST_METHOD': 'DELETE',
4704+ 'swift.cache': FakeMemcache()},
4705+ headers={'X-Auth-Admin-User': '.super_admin',
4706+ 'X-Auth-Admin-Key': 'supertest'}
4707+ ).get_response(self.test_auth)
4708+ self.assertEquals(resp.status_int, 204)
4709+ self.assertEquals(self.test_auth.app.calls, 6)
4710+ self.assertEquals(conn.calls, 1)
4711+
4712+ def test_delete_account_success_missing_account_container_at_end(self):
4713+ conn = FakeConn(iter([
4714+ # DELETE of storage account itself
4715+ ('204 No Content', {}, '')]))
4716+ self.test_auth.get_conn = lambda x: conn
4717+ self.test_auth.app = FakeApp(iter([
4718+ # Account's container listing, checking for users
4719+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
4720+ json.dumps([
4721+ {"name": ".services", "hash": "etag", "bytes": 112,
4722+ "content_type": "application/octet-stream",
4723+ "last_modified": "2010-12-03T17:16:27.618110"}])),
4724+ # Account's container listing, checking for users (continuation)
4725+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'),
4726+ # GET the .services object
4727+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
4728+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
4729+ # DELETE the .services object
4730+ ('204 No Content', {}, ''),
4731+ # DELETE the .account_id mapping object
4732+ ('204 No Content', {}, ''),
4733+ # DELETE the account container
4734+ ('404 Not Found', {}, '')]))
4735+ resp = Request.blank('/auth/v2/act',
4736+ environ={'REQUEST_METHOD': 'DELETE',
4737+ 'swift.cache': FakeMemcache()},
4738+ headers={'X-Auth-Admin-User': '.super_admin',
4739+ 'X-Auth-Admin-Key': 'supertest'}
4740+ ).get_response(self.test_auth)
4741+ self.assertEquals(resp.status_int, 204)
4742+ self.assertEquals(self.test_auth.app.calls, 6)
4743+ self.assertEquals(conn.calls, 1)
4744+
4745+ def test_delete_account_fail_bad_creds(self):
4746+ self.test_auth.app = FakeApp(iter([
4747+ # GET of user object
4748+ ('404 Not Found', {}, '')]))
4749+ resp = Request.blank('/auth/v2/act',
4750+ environ={'REQUEST_METHOD': 'DELETE',
4751+ 'swift.cache': FakeMemcache()},
4752+ headers={'X-Auth-Admin-User': 'super:admin',
4753+ 'X-Auth-Admin-Key': 'supertest'},
4754+ ).get_response(self.test_auth)
4755+ self.assertEquals(resp.status_int, 403)
4756+ self.assertEquals(self.test_auth.app.calls, 1)
4757+
4758+ self.test_auth.app = FakeApp(iter([
4759+ # GET of user object (account admin, but not reseller admin)
4760+ ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"},
4761+ {"name": "test"}, {"name": ".admin"}],
4762+ "auth": "plaintext:key"}))]))
4763+ resp = Request.blank('/auth/v2/act',
4764+ environ={'REQUEST_METHOD': 'DELETE',
4765+ 'swift.cache': FakeMemcache()},
4766+ headers={'X-Auth-Admin-User': 'act:adm',
4767+ 'X-Auth-Admin-Key': 'key'},
4768+ ).get_response(self.test_auth)
4769+ self.assertEquals(resp.status_int, 403)
4770+ self.assertEquals(self.test_auth.app.calls, 1)
4771+
4772+ self.test_auth.app = FakeApp(iter([
4773+ # GET of user object (regular user)
4774+ ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"},
4775+ {"name": "test"}], "auth": "plaintext:key"}))]))
4776+ resp = Request.blank('/auth/v2/act',
4777+ environ={'REQUEST_METHOD': 'DELETE',
4778+ 'swift.cache': FakeMemcache()},
4779+ headers={'X-Auth-Admin-User': 'act:usr',
4780+ 'X-Auth-Admin-Key': 'key'},
4781+ ).get_response(self.test_auth)
4782+ self.assertEquals(resp.status_int, 403)
4783+ self.assertEquals(self.test_auth.app.calls, 1)
4784+
4785+ def test_delete_account_fail_invalid_account_name(self):
4786+ resp = Request.blank('/auth/v2/.act',
4787+ environ={'REQUEST_METHOD': 'DELETE'},
4788+ headers={'X-Auth-Admin-User': '.super_admin',
4789+ 'X-Auth-Admin-Key': 'supertest'}
4790+ ).get_response(self.test_auth)
4791+ self.assertEquals(resp.status_int, 400)
4792+
4793+ def test_delete_account_fail_not_found(self):
4794+ self.test_auth.app = FakeApp(iter([
4795+ # Account's container listing, checking for users
4796+ ('404 Not Found', {}, '')]))
4797+ resp = Request.blank('/auth/v2/act',
4798+ environ={'REQUEST_METHOD': 'DELETE',
4799+ 'swift.cache': FakeMemcache()},
4800+ headers={'X-Auth-Admin-User': '.super_admin',
4801+ 'X-Auth-Admin-Key': 'supertest'}
4802+ ).get_response(self.test_auth)
4803+ self.assertEquals(resp.status_int, 404)
4804+ self.assertEquals(self.test_auth.app.calls, 1)
4805+
4806+ def test_delete_account_fail_not_found_concurrency(self):
4807+ self.test_auth.app = FakeApp(iter([
4808+ # Account's container listing, checking for users
4809+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
4810+ json.dumps([
4811+ {"name": ".services", "hash": "etag", "bytes": 112,
4812+ "content_type": "application/octet-stream",
4813+ "last_modified": "2010-12-03T17:16:27.618110"}])),
4814+ # Account's container listing, checking for users (continuation)
4815+ ('404 Not Found', {}, '')]))
4816+ resp = Request.blank('/auth/v2/act',
4817+ environ={'REQUEST_METHOD': 'DELETE',
4818+ 'swift.cache': FakeMemcache()},
4819+ headers={'X-Auth-Admin-User': '.super_admin',
4820+ 'X-Auth-Admin-Key': 'supertest'}
4821+ ).get_response(self.test_auth)
4822+ self.assertEquals(resp.status_int, 404)
4823+ self.assertEquals(self.test_auth.app.calls, 2)
4824+
4825+ def test_delete_account_fail_list_account(self):
4826+ self.test_auth.app = FakeApp(iter([
4827+ # Account's container listing, checking for users
4828+ ('503 Service Unavailable', {}, '')]))
4829+ resp = Request.blank('/auth/v2/act',
4830+ environ={'REQUEST_METHOD': 'DELETE',
4831+ 'swift.cache': FakeMemcache()},
4832+ headers={'X-Auth-Admin-User': '.super_admin',
4833+ 'X-Auth-Admin-Key': 'supertest'}
4834+ ).get_response(self.test_auth)
4835+ self.assertEquals(resp.status_int, 500)
4836+ self.assertEquals(self.test_auth.app.calls, 1)
4837+
4838+ def test_delete_account_fail_list_account_concurrency(self):
4839+ self.test_auth.app = FakeApp(iter([
4840+ # Account's container listing, checking for users
4841+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
4842+ json.dumps([
4843+ {"name": ".services", "hash": "etag", "bytes": 112,
4844+ "content_type": "application/octet-stream",
4845+ "last_modified": "2010-12-03T17:16:27.618110"}])),
4846+ # Account's container listing, checking for users (continuation)
4847+ ('503 Service Unavailable', {}, '')]))
4848+ resp = Request.blank('/auth/v2/act',
4849+ environ={'REQUEST_METHOD': 'DELETE',
4850+ 'swift.cache': FakeMemcache()},
4851+ headers={'X-Auth-Admin-User': '.super_admin',
4852+ 'X-Auth-Admin-Key': 'supertest'}
4853+ ).get_response(self.test_auth)
4854+ self.assertEquals(resp.status_int, 500)
4855+ self.assertEquals(self.test_auth.app.calls, 2)
4856+
4857+ def test_delete_account_fail_has_users(self):
4858+ self.test_auth.app = FakeApp(iter([
4859+ # Account's container listing, checking for users
4860+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
4861+ json.dumps([
4862+ {"name": ".services", "hash": "etag", "bytes": 112,
4863+ "content_type": "application/octet-stream",
4864+ "last_modified": "2010-12-03T17:16:27.618110"},
4865+ {"name": "tester", "hash": "etag", "bytes": 104,
4866+ "content_type": "application/octet-stream",
4867+ "last_modified": "2010-12-03T17:16:27.736680"}]))]))
4868+ resp = Request.blank('/auth/v2/act',
4869+ environ={'REQUEST_METHOD': 'DELETE',
4870+ 'swift.cache': FakeMemcache()},
4871+ headers={'X-Auth-Admin-User': '.super_admin',
4872+ 'X-Auth-Admin-Key': 'supertest'}
4873+ ).get_response(self.test_auth)
4874+ self.assertEquals(resp.status_int, 409)
4875+ self.assertEquals(self.test_auth.app.calls, 1)
4876+
4877+ def test_delete_account_fail_has_users2(self):
4878+ self.test_auth.app = FakeApp(iter([
4879+ # Account's container listing, checking for users
4880+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
4881+ json.dumps([
4882+ {"name": ".services", "hash": "etag", "bytes": 112,
4883+ "content_type": "application/octet-stream",
4884+ "last_modified": "2010-12-03T17:16:27.618110"}])),
4885+ # Account's container listing, checking for users (continuation)
4886+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
4887+ json.dumps([
4888+ {"name": "tester", "hash": "etag", "bytes": 104,
4889+ "content_type": "application/octet-stream",
4890+ "last_modified": "2010-12-03T17:16:27.736680"}]))]))
4891+ resp = Request.blank('/auth/v2/act',
4892+ environ={'REQUEST_METHOD': 'DELETE',
4893+ 'swift.cache': FakeMemcache()},
4894+ headers={'X-Auth-Admin-User': '.super_admin',
4895+ 'X-Auth-Admin-Key': 'supertest'}
4896+ ).get_response(self.test_auth)
4897+ self.assertEquals(resp.status_int, 409)
4898+ self.assertEquals(self.test_auth.app.calls, 2)
4899+
4900+ def test_delete_account_fail_get_services(self):
4901+ self.test_auth.app = FakeApp(iter([
4902+ # Account's container listing, checking for users
4903+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
4904+ json.dumps([
4905+ {"name": ".services", "hash": "etag", "bytes": 112,
4906+ "content_type": "application/octet-stream",
4907+ "last_modified": "2010-12-03T17:16:27.618110"}])),
4908+ # Account's container listing, checking for users (continuation)
4909+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'),
4910+ # GET the .services object
4911+ ('503 Service Unavailable', {}, '')]))
4912+ resp = Request.blank('/auth/v2/act',
4913+ environ={'REQUEST_METHOD': 'DELETE',
4914+ 'swift.cache': FakeMemcache()},
4915+ headers={'X-Auth-Admin-User': '.super_admin',
4916+ 'X-Auth-Admin-Key': 'supertest'}
4917+ ).get_response(self.test_auth)
4918+ self.assertEquals(resp.status_int, 500)
4919+ self.assertEquals(self.test_auth.app.calls, 3)
4920+
4921+ def test_delete_account_fail_delete_storage_account(self):
4922+ conn = FakeConn(iter([
4923+ # DELETE of storage account itself
4924+ ('409 Conflict', {}, '')]))
4925+ self.test_auth.get_conn = lambda x: conn
4926+ self.test_auth.app = FakeApp(iter([
4927+ # Account's container listing, checking for users
4928+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
4929+ json.dumps([
4930+ {"name": ".services", "hash": "etag", "bytes": 112,
4931+ "content_type": "application/octet-stream",
4932+ "last_modified": "2010-12-03T17:16:27.618110"}])),
4933+ # Account's container listing, checking for users (continuation)
4934+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'),
4935+ # GET the .services object
4936+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
4937+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
4938+ resp = Request.blank('/auth/v2/act',
4939+ environ={'REQUEST_METHOD': 'DELETE',
4940+ 'swift.cache': FakeMemcache()},
4941+ headers={'X-Auth-Admin-User': '.super_admin',
4942+ 'X-Auth-Admin-Key': 'supertest'}
4943+ ).get_response(self.test_auth)
4944+ self.assertEquals(resp.status_int, 409)
4945+ self.assertEquals(self.test_auth.app.calls, 3)
4946+ self.assertEquals(conn.calls, 1)
4947+
4948+ def test_delete_account_fail_delete_storage_account2(self):
4949+ conn = FakeConn(iter([
4950+ # DELETE of storage account itself
4951+ ('204 No Content', {}, ''),
4952+ # DELETE of storage account itself
4953+ ('409 Conflict', {}, '')]))
4954+ self.test_auth.get_conn = lambda x: conn
4955+ self.test_auth.app = FakeApp(iter([
4956+ # Account's container listing, checking for users
4957+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
4958+ json.dumps([
4959+ {"name": ".services", "hash": "etag", "bytes": 112,
4960+ "content_type": "application/octet-stream",
4961+ "last_modified": "2010-12-03T17:16:27.618110"}])),
4962+ # Account's container listing, checking for users (continuation)
4963+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'),
4964+ # GET the .services object
4965+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
4966+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa",
4967+ "other": "http://127.0.0.1:8080/v1/AUTH_cfa2"}}))]))
4968+ resp = Request.blank('/auth/v2/act',
4969+ environ={'REQUEST_METHOD': 'DELETE',
4970+ 'swift.cache': FakeMemcache()},
4971+ headers={'X-Auth-Admin-User': '.super_admin',
4972+ 'X-Auth-Admin-Key': 'supertest'}
4973+ ).get_response(self.test_auth)
4974+ self.assertEquals(resp.status_int, 500)
4975+ self.assertEquals(self.test_auth.app.calls, 3)
4976+ self.assertEquals(conn.calls, 2)
4977+
4978+ def test_delete_account_fail_delete_storage_account3(self):
4979+ conn = FakeConn(iter([
4980+ # DELETE of storage account itself
4981+ ('503 Service Unavailable', {}, '')]))
4982+ self.test_auth.get_conn = lambda x: conn
4983+ self.test_auth.app = FakeApp(iter([
4984+ # Account's container listing, checking for users
4985+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
4986+ json.dumps([
4987+ {"name": ".services", "hash": "etag", "bytes": 112,
4988+ "content_type": "application/octet-stream",
4989+ "last_modified": "2010-12-03T17:16:27.618110"}])),
4990+ # Account's container listing, checking for users (continuation)
4991+ ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'),
4992+ # GET the .services object
4993+ ('200 Ok', {}, json.dumps({"storage": {"default": "local",
4994+ "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
4995+ resp = Request.blank('/auth/v2/act',
4996+ environ={'REQUEST_METHOD': 'DELETE',
4997+ 'swift.cache': FakeMemcache()},
4998+ headers={'X-Auth-Admin-User': '.super_admin',
4999+ 'X-Auth-Admin-Key': 'supertest'}
5000+ ).get_response(self.test_auth)
The diff has been truncated for viewing.