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

Proposed by gholt
Status: Merged
Approved by: David Goetz
Approved revision: 307
Merged at revision: 306
Proposed branch: lp:~gholt/swift/deswauth
Merge into: lp:~hudson-openstack/swift/trunk
Diff against target: 6463 lines (+675/-5219)
22 files modified
bin/swauth-add-account (+0/-68)
bin/swauth-add-user (+0/-93)
bin/swauth-cleanup-tokens (+0/-118)
bin/swauth-delete-account (+0/-60)
bin/swauth-delete-user (+0/-60)
bin/swauth-list (+0/-86)
bin/swauth-prep (+0/-59)
bin/swauth-set-account-service (+0/-73)
doc/source/admin_guide.rst (+0/-16)
doc/source/deployment_guide.rst (+33/-28)
doc/source/development_auth.rst (+7/-7)
doc/source/development_saio.rst (+11/-19)
doc/source/howto_installmultinode.rst (+11/-52)
doc/source/misc.rst (+6/-6)
doc/source/overview_auth.rst (+8/-151)
etc/proxy-server.conf-sample (+24/-17)
setup.py (+1/-5)
swift/common/middleware/staticweb.py (+1/-1)
swift/common/middleware/swauth.py (+0/-1374)
swift/common/middleware/tempauth.py (+482/-0)
test/probe/common.py (+19/-21)
test/unit/common/middleware/test_tempauth.py (+72/-2905)
To merge this branch: bzr merge lp:~gholt/swift/deswauth
Reviewer Review Type Date Requested Status
David Goetz (community) Approve
John Dickinson Approve
Review via email: mp+62392@code.launchpad.net

Description of the change

In light of the upcoming Keystone auth service, which should become the standard OpenStack auth service, Swauth will need to move to its own project. Because we don't want the Swift project to require Keystone (or Swauth or any other auth service for that matter) we need a "placeholder". In this proposal I'm adding a TempAuth to fill our auth testing needs within Swift and removing Swauth.

To quickly change from Swauth on a standard SAIO install, change swauth in your pipeline to tempauth and add the following section:

[filter:tempauth]
use = egg:swift#tempauth
user_test_tester = testing .admin
user_test2_tester2 = testing2 .admin
user_test_tester3 = testing3
user_system_root = testpass .admin .reseller_admin

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

Merged from trunk

306. By gholt

Made probe tests more resilent against timing issues.

Revision history for this message
John Dickinson (notmyname) wrote :

quite a large diff, but it's mostly simple changes to remove swauth. The tempauth looks fine.

review: Approve
lp:~gholt/swift/deswauth updated
307. By gholt

tempauth: Check and make all handled accounts

Revision history for this message
David Goetz (david-goetz) wrote :

looks good

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== removed file 'bin/swauth-add-account'
--- bin/swauth-add-account 2011-04-18 16:08:48 +0000
+++ bin/swauth-add-account 1970-01-01 00:00:00 +0000
@@ -1,68 +0,0 @@
1#!/usr/bin/env python
2# Copyright (c) 2010 OpenStack, LLC.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import gettext
18from optparse import OptionParser
19from os.path import basename
20from sys import argv, exit
21
22from swift.common.bufferedhttp import http_connect_raw as http_connect
23from swift.common.utils import urlparse
24
25
26if __name__ == '__main__':
27 gettext.install('swift', unicode=1)
28 parser = OptionParser(usage='Usage: %prog [options] <account>')
29 parser.add_option('-s', '--suffix', dest='suffix',
30 default='', help='The suffix to use with the reseller prefix as the '
31 'storage account name (default: <randomly-generated-uuid4>) Note: If '
32 'the account already exists, this will have no effect on existing '
33 'service URLs. Those will need to be updated with '
34 'swauth-set-account-service')
35 parser.add_option('-A', '--admin-url', dest='admin_url',
36 default='http://127.0.0.1:8080/auth/', help='The URL to the auth '
37 'subsystem (default: http://127.0.0.1:8080/auth/)')
38 parser.add_option('-U', '--admin-user', dest='admin_user',
39 default='.super_admin', help='The user with admin rights to add users '
40 '(default: .super_admin).')
41 parser.add_option('-K', '--admin-key', dest='admin_key',
42 help='The key for the user with admin rights to add users.')
43 args = argv[1:]
44 if not args:
45 args.append('-h')
46 (options, args) = parser.parse_args(args)
47 if len(args) != 1:
48 parser.parse_args(['-h'])
49 account = args[0]
50 parsed = urlparse(options.admin_url)
51 if parsed.scheme not in ('http', 'https'):
52 raise Exception('Cannot handle protocol scheme %s for url %s' %
53 (parsed.scheme, repr(options.admin_url)))
54 parsed_path = parsed.path
55 if not parsed_path:
56 parsed_path = '/'
57 elif parsed_path[-1] != '/':
58 parsed_path += '/'
59 path = '%sv2/%s' % (parsed_path, account)
60 headers = {'X-Auth-Admin-User': options.admin_user,
61 'X-Auth-Admin-Key': options.admin_key}
62 if options.suffix:
63 headers['X-Account-Suffix'] = options.suffix
64 conn = http_connect(parsed.hostname, parsed.port, 'PUT', path, headers,
65 ssl=(parsed.scheme == 'https'))
66 resp = conn.getresponse()
67 if resp.status // 100 != 2:
68 exit('Account creation failed: %s %s' % (resp.status, resp.reason))
690
=== removed file 'bin/swauth-add-user'
--- bin/swauth-add-user 2011-04-18 16:08:48 +0000
+++ bin/swauth-add-user 1970-01-01 00:00:00 +0000
@@ -1,93 +0,0 @@
1#!/usr/bin/env python
2# Copyright (c) 2010 OpenStack, LLC.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import gettext
18from optparse import OptionParser
19from os.path import basename
20from sys import argv, exit
21
22from swift.common.bufferedhttp import http_connect_raw as http_connect
23from swift.common.utils import urlparse
24
25
26if __name__ == '__main__':
27 gettext.install('swift', unicode=1)
28 parser = OptionParser(
29 usage='Usage: %prog [options] <account> <user> <password>')
30 parser.add_option('-a', '--admin', dest='admin', action='store_true',
31 default=False, help='Give the user administrator access; otherwise '
32 'the user will only have access to containers specifically allowed '
33 'with ACLs.')
34 parser.add_option('-r', '--reseller-admin', dest='reseller_admin',
35 action='store_true', default=False, help='Give the user full reseller '
36 'administrator access, giving them full access to all accounts within '
37 'the reseller, including the ability to create new accounts. Creating '
38 'a new reseller admin requires super_admin rights.')
39 parser.add_option('-s', '--suffix', dest='suffix',
40 default='', help='The suffix to use with the reseller prefix as the '
41 'storage account name (default: <randomly-generated-uuid4>) Note: If '
42 'the account already exists, this will have no effect on existing '
43 'service URLs. Those will need to be updated with '
44 'swauth-set-account-service')
45 parser.add_option('-A', '--admin-url', dest='admin_url',
46 default='http://127.0.0.1:8080/auth/', help='The URL to the auth '
47 'subsystem (default: http://127.0.0.1:8080/auth/')
48 parser.add_option('-U', '--admin-user', dest='admin_user',
49 default='.super_admin', help='The user with admin rights to add users '
50 '(default: .super_admin).')
51 parser.add_option('-K', '--admin-key', dest='admin_key',
52 help='The key for the user with admin rights to add users.')
53 args = argv[1:]
54 if not args:
55 args.append('-h')
56 (options, args) = parser.parse_args(args)
57 if len(args) != 3:
58 parser.parse_args(['-h'])
59 account, user, password = args
60 parsed = urlparse(options.admin_url)
61 if parsed.scheme not in ('http', 'https'):
62 raise Exception('Cannot handle protocol scheme %s for url %s' %
63 (parsed.scheme, repr(options.admin_url)))
64 parsed_path = parsed.path
65 if not parsed_path:
66 parsed_path = '/'
67 elif parsed_path[-1] != '/':
68 parsed_path += '/'
69 # Ensure the account exists
70 path = '%sv2/%s' % (parsed_path, account)
71 headers = {'X-Auth-Admin-User': options.admin_user,
72 'X-Auth-Admin-Key': options.admin_key}
73 if options.suffix:
74 headers['X-Account-Suffix'] = options.suffix
75 conn = http_connect(parsed.hostname, parsed.port, 'PUT', path, headers,
76 ssl=(parsed.scheme == 'https'))
77 resp = conn.getresponse()
78 if resp.status // 100 != 2:
79 print 'Account creation failed: %s %s' % (resp.status, resp.reason)
80 # Add the user
81 path = '%sv2/%s/%s' % (parsed_path, account, user)
82 headers = {'X-Auth-Admin-User': options.admin_user,
83 'X-Auth-Admin-Key': options.admin_key,
84 'X-Auth-User-Key': password}
85 if options.admin:
86 headers['X-Auth-User-Admin'] = 'true'
87 if options.reseller_admin:
88 headers['X-Auth-User-Reseller-Admin'] = 'true'
89 conn = http_connect(parsed.hostname, parsed.port, 'PUT', path, headers,
90 ssl=(parsed.scheme == 'https'))
91 resp = conn.getresponse()
92 if resp.status // 100 != 2:
93 exit('User creation failed: %s %s' % (resp.status, resp.reason))
940
=== removed file 'bin/swauth-cleanup-tokens'
--- bin/swauth-cleanup-tokens 2011-04-18 16:08:48 +0000
+++ bin/swauth-cleanup-tokens 1970-01-01 00:00:00 +0000
@@ -1,118 +0,0 @@
1#!/usr/bin/env python
2# Copyright (c) 2010 OpenStack, LLC.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17try:
18 import simplejson as json
19except ImportError:
20 import json
21import gettext
22import re
23from datetime import datetime, timedelta
24from optparse import OptionParser
25from sys import argv, exit
26from time import sleep, time
27
28from swift.common.client import Connection, ClientException
29
30
31if __name__ == '__main__':
32 gettext.install('swift', unicode=1)
33 parser = OptionParser(usage='Usage: %prog [options]')
34 parser.add_option('-t', '--token-life', dest='token_life',
35 default='86400', help='The expected life of tokens; token objects '
36 'modified more than this number of seconds ago will be checked for '
37 'expiration (default: 86400).')
38 parser.add_option('-s', '--sleep', dest='sleep',
39 default='0.1', help='The number of seconds to sleep between token '
40 'checks (default: 0.1)')
41 parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
42 default=False, help='Outputs everything done instead of just the '
43 'deletions.')
44 parser.add_option('-A', '--admin-url', dest='admin_url',
45 default='http://127.0.0.1:8080/auth/', help='The URL to the auth '
46 'subsystem (default: http://127.0.0.1:8080/auth/)')
47 parser.add_option('-K', '--admin-key', dest='admin_key',
48 help='The key for .super_admin.')
49 args = argv[1:]
50 if not args:
51 args.append('-h')
52 (options, args) = parser.parse_args(args)
53 if len(args) != 0:
54 parser.parse_args(['-h'])
55 options.admin_url = options.admin_url.rstrip('/')
56 if not options.admin_url.endswith('/v1.0'):
57 options.admin_url += '/v1.0'
58 options.admin_user = '.super_admin:.super_admin'
59 options.token_life = timedelta(0, float(options.token_life))
60 options.sleep = float(options.sleep)
61 conn = Connection(options.admin_url, options.admin_user, options.admin_key)
62 for x in xrange(16):
63 container = '.token_%x' % x
64 marker = None
65 while True:
66 if options.verbose:
67 print 'GET %s?marker=%s' % (container, marker)
68 try:
69 objs = conn.get_container(container, marker=marker)[1]
70 except ClientException, e:
71 if e.http_status == 404:
72 exit('Container %s not found. swauth-prep needs to be '
73 'rerun' % (container))
74 else:
75 exit('Object listing on container %s failed with status '
76 'code %d' % (container, e.http_status))
77 if objs:
78 marker = objs[-1]['name']
79 else:
80 if options.verbose:
81 print 'No more objects in %s' % container
82 break
83 for obj in objs:
84 last_modified = datetime(*map(int, re.split('[^\d]',
85 obj['last_modified'])[:-1]))
86 ago = datetime.utcnow() - last_modified
87 if ago > options.token_life:
88 if options.verbose:
89 print '%s/%s last modified %ss ago; investigating' % \
90 (container, obj['name'],
91 ago.days * 86400 + ago.seconds)
92 print 'GET %s/%s' % (container, obj['name'])
93 detail = conn.get_object(container, obj['name'])[1]
94 detail = json.loads(detail)
95 if detail['expires'] < time():
96 if options.verbose:
97 print '%s/%s expired %ds ago; deleting' % \
98 (container, obj['name'],
99 time() - detail['expires'])
100 print 'DELETE %s/%s' % (container, obj['name'])
101 try:
102 conn.delete_object(container, obj['name'])
103 except ClientException, e:
104 if e.http_status != 404:
105 print 'DELETE of %s/%s failed with status ' \
106 'code %d' % (container, obj['name'],
107 e.http_status)
108 elif options.verbose:
109 print "%s/%s won't expire for %ds; skipping" % \
110 (container, obj['name'],
111 detail['expires'] - time())
112 elif options.verbose:
113 print '%s/%s last modified %ss ago; skipping' % \
114 (container, obj['name'],
115 ago.days * 86400 + ago.seconds)
116 sleep(options.sleep)
117 if options.verbose:
118 print 'Done.'
1190
=== removed file 'bin/swauth-delete-account'
--- bin/swauth-delete-account 2011-04-18 16:08:48 +0000
+++ bin/swauth-delete-account 1970-01-01 00:00:00 +0000
@@ -1,60 +0,0 @@
1#!/usr/bin/env python
2# Copyright (c) 2010 OpenStack, LLC.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import gettext
18from optparse import OptionParser
19from os.path import basename
20from sys import argv, exit
21
22from swift.common.bufferedhttp import http_connect_raw as http_connect
23from swift.common.utils import urlparse
24
25
26if __name__ == '__main__':
27 gettext.install('swift', unicode=1)
28 parser = OptionParser(usage='Usage: %prog [options] <account>')
29 parser.add_option('-A', '--admin-url', dest='admin_url',
30 default='http://127.0.0.1:8080/auth/', help='The URL to the auth '
31 'subsystem (default: http://127.0.0.1:8080/auth/')
32 parser.add_option('-U', '--admin-user', dest='admin_user',
33 default='.super_admin', help='The user with admin rights to add users '
34 '(default: .super_admin).')
35 parser.add_option('-K', '--admin-key', dest='admin_key',
36 help='The key for the user with admin rights to add users.')
37 args = argv[1:]
38 if not args:
39 args.append('-h')
40 (options, args) = parser.parse_args(args)
41 if len(args) != 1:
42 parser.parse_args(['-h'])
43 account = args[0]
44 parsed = urlparse(options.admin_url)
45 if parsed.scheme not in ('http', 'https'):
46 raise Exception('Cannot handle protocol scheme %s for url %s' %
47 (parsed.scheme, repr(options.admin_url)))
48 parsed_path = parsed.path
49 if not parsed_path:
50 parsed_path = '/'
51 elif parsed_path[-1] != '/':
52 parsed_path += '/'
53 path = '%sv2/%s' % (parsed_path, account)
54 headers = {'X-Auth-Admin-User': options.admin_user,
55 'X-Auth-Admin-Key': options.admin_key}
56 conn = http_connect(parsed.hostname, parsed.port, 'DELETE', path, headers,
57 ssl=(parsed.scheme == 'https'))
58 resp = conn.getresponse()
59 if resp.status // 100 != 2:
60 exit('Account deletion failed: %s %s' % (resp.status, resp.reason))
610
=== removed file 'bin/swauth-delete-user'
--- bin/swauth-delete-user 2011-04-18 16:08:48 +0000
+++ bin/swauth-delete-user 1970-01-01 00:00:00 +0000
@@ -1,60 +0,0 @@
1#!/usr/bin/env python
2# Copyright (c) 2010 OpenStack, LLC.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import gettext
18from optparse import OptionParser
19from os.path import basename
20from sys import argv, exit
21
22from swift.common.bufferedhttp import http_connect_raw as http_connect
23from swift.common.utils import urlparse
24
25
26if __name__ == '__main__':
27 gettext.install('swift', unicode=1)
28 parser = OptionParser(usage='Usage: %prog [options] <account> <user>')
29 parser.add_option('-A', '--admin-url', dest='admin_url',
30 default='http://127.0.0.1:8080/auth/', help='The URL to the auth '
31 'subsystem (default: http://127.0.0.1:8080/auth/')
32 parser.add_option('-U', '--admin-user', dest='admin_user',
33 default='.super_admin', help='The user with admin rights to add users '
34 '(default: .super_admin).')
35 parser.add_option('-K', '--admin-key', dest='admin_key',
36 help='The key for the user with admin rights to add users.')
37 args = argv[1:]
38 if not args:
39 args.append('-h')
40 (options, args) = parser.parse_args(args)
41 if len(args) != 2:
42 parser.parse_args(['-h'])
43 account, user = args
44 parsed = urlparse(options.admin_url)
45 if parsed.scheme not in ('http', 'https'):
46 raise Exception('Cannot handle protocol scheme %s for url %s' %
47 (parsed.scheme, repr(options.admin_url)))
48 parsed_path = parsed.path
49 if not parsed_path:
50 parsed_path = '/'
51 elif parsed_path[-1] != '/':
52 parsed_path += '/'
53 path = '%sv2/%s/%s' % (parsed_path, account, user)
54 headers = {'X-Auth-Admin-User': options.admin_user,
55 'X-Auth-Admin-Key': options.admin_key}
56 conn = http_connect(parsed.hostname, parsed.port, 'DELETE', path, headers,
57 ssl=(parsed.scheme == 'https'))
58 resp = conn.getresponse()
59 if resp.status // 100 != 2:
60 exit('User deletion failed: %s %s' % (resp.status, resp.reason))
610
=== removed file 'bin/swauth-list'
--- bin/swauth-list 2011-04-18 16:08:48 +0000
+++ bin/swauth-list 1970-01-01 00:00:00 +0000
@@ -1,86 +0,0 @@
1#!/usr/bin/env python
2# Copyright (c) 2010 OpenStack, LLC.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17try:
18 import simplejson as json
19except ImportError:
20 import json
21import gettext
22from optparse import OptionParser
23from os.path import basename
24from sys import argv, exit
25
26from swift.common.bufferedhttp import http_connect_raw as http_connect
27from swift.common.utils import urlparse
28
29
30if __name__ == '__main__':
31 gettext.install('swift', unicode=1)
32 parser = OptionParser(usage='''
33Usage: %prog [options] [account] [user]
34
35If [account] and [user] are omitted, a list of accounts will be output.
36
37If [account] is included but not [user], an account's information will be
38output, including a list of users within the account.
39
40If [account] and [user] are included, the user's information will be output,
41including a list of groups the user belongs to.
42
43If the [user] is '.groups', the active groups for the account will be listed.
44'''.strip())
45 parser.add_option('-p', '--plain-text', dest='plain_text',
46 action='store_true', default=False, help='Changes the output from '
47 'JSON to plain text. This will cause an account to list only the '
48 'users and a user to list only the groups.')
49 parser.add_option('-A', '--admin-url', dest='admin_url',
50 default='http://127.0.0.1:8080/auth/', help='The URL to the auth '
51 'subsystem (default: http://127.0.0.1:8080/auth/')
52 parser.add_option('-U', '--admin-user', dest='admin_user',
53 default='.super_admin', help='The user with admin rights to add users '
54 '(default: .super_admin).')
55 parser.add_option('-K', '--admin-key', dest='admin_key',
56 help='The key for the user with admin rights to add users.')
57 args = argv[1:]
58 if not args:
59 args.append('-h')
60 (options, args) = parser.parse_args(args)
61 if len(args) > 2:
62 parser.parse_args(['-h'])
63 parsed = urlparse(options.admin_url)
64 if parsed.scheme not in ('http', 'https'):
65 raise Exception('Cannot handle protocol scheme %s for url %s' %
66 (parsed.scheme, repr(options.admin_url)))
67 parsed_path = parsed.path
68 if not parsed_path:
69 parsed_path = '/'
70 elif parsed_path[-1] != '/':
71 parsed_path += '/'
72 path = '%sv2/%s' % (parsed_path, '/'.join(args))
73 headers = {'X-Auth-Admin-User': options.admin_user,
74 'X-Auth-Admin-Key': options.admin_key}
75 conn = http_connect(parsed.hostname, parsed.port, 'GET', path, headers,
76 ssl=(parsed.scheme == 'https'))
77 resp = conn.getresponse()
78 body = resp.read()
79 if resp.status // 100 != 2:
80 exit('List failed: %s %s' % (resp.status, resp.reason))
81 if options.plain_text:
82 info = json.loads(body)
83 for group in info[['accounts', 'users', 'groups'][len(args)]]:
84 print group['name']
85 else:
86 print body
870
=== removed file 'bin/swauth-prep'
--- bin/swauth-prep 2011-04-18 16:08:48 +0000
+++ bin/swauth-prep 1970-01-01 00:00:00 +0000
@@ -1,59 +0,0 @@
1#!/usr/bin/env python
2# Copyright (c) 2010 OpenStack, LLC.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import gettext
18from optparse import OptionParser
19from os.path import basename
20from sys import argv, exit
21
22from swift.common.bufferedhttp import http_connect_raw as http_connect
23from swift.common.utils import urlparse
24
25
26if __name__ == '__main__':
27 gettext.install('swift', unicode=1)
28 parser = OptionParser(usage='Usage: %prog [options]')
29 parser.add_option('-A', '--admin-url', dest='admin_url',
30 default='http://127.0.0.1:8080/auth/', help='The URL to the auth '
31 'subsystem (default: http://127.0.0.1:8080/auth/')
32 parser.add_option('-U', '--admin-user', dest='admin_user',
33 default='.super_admin', help='The user with admin rights to add users '
34 '(default: .super_admin).')
35 parser.add_option('-K', '--admin-key', dest='admin_key',
36 help='The key for the user with admin rights to add users.')
37 args = argv[1:]
38 if not args:
39 args.append('-h')
40 (options, args) = parser.parse_args(args)
41 if args:
42 parser.parse_args(['-h'])
43 parsed = urlparse(options.admin_url)
44 if parsed.scheme not in ('http', 'https'):
45 raise Exception('Cannot handle protocol scheme %s for url %s' %
46 (parsed.scheme, repr(options.admin_url)))
47 parsed_path = parsed.path
48 if not parsed_path:
49 parsed_path = '/'
50 elif parsed_path[-1] != '/':
51 parsed_path += '/'
52 path = '%sv2/.prep' % parsed_path
53 headers = {'X-Auth-Admin-User': options.admin_user,
54 'X-Auth-Admin-Key': options.admin_key}
55 conn = http_connect(parsed.hostname, parsed.port, 'POST', path, headers,
56 ssl=(parsed.scheme == 'https'))
57 resp = conn.getresponse()
58 if resp.status // 100 != 2:
59 exit('Auth subsystem prep failed: %s %s' % (resp.status, resp.reason))
600
=== removed file 'bin/swauth-set-account-service'
--- bin/swauth-set-account-service 2011-04-18 16:08:48 +0000
+++ bin/swauth-set-account-service 1970-01-01 00:00:00 +0000
@@ -1,73 +0,0 @@
1#!/usr/bin/env python
2# Copyright (c) 2010 OpenStack, LLC.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17try:
18 import simplejson as json
19except ImportError:
20 import json
21import gettext
22from optparse import OptionParser
23from os.path import basename
24from sys import argv, exit
25
26from swift.common.bufferedhttp import http_connect_raw as http_connect
27from swift.common.utils import urlparse
28
29
30if __name__ == '__main__':
31 gettext.install('swift', unicode=1)
32 parser = OptionParser(usage='''
33Usage: %prog [options] <account> <service> <name> <value>
34
35Sets a service URL for an account. Can only be set by a reseller admin.
36
37Example: %prog -K swauthkey test storage local http://127.0.0.1:8080/v1/AUTH_018c3946-23f8-4efb-a8fb-b67aae8e4162
38'''.strip())
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) != 4:
52 parser.parse_args(['-h'])
53 account, service, name, url = args
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 parsed_path = parsed.path
59 if not parsed_path:
60 parsed_path = '/'
61 elif parsed_path[-1] != '/':
62 parsed_path += '/'
63 path = '%sv2/%s/.services' % (parsed_path, account)
64 body = json.dumps({service: {name: url}})
65 headers = {'Content-Length': str(len(body)),
66 'X-Auth-Admin-User': options.admin_user,
67 'X-Auth-Admin-Key': options.admin_key}
68 conn = http_connect(parsed.hostname, parsed.port, 'POST', path, headers,
69 ssl=(parsed.scheme == 'https'))
70 conn.send(body)
71 resp = conn.getresponse()
72 if resp.status // 100 != 2:
73 exit('Service set failed: %s %s' % (resp.status, resp.reason))
740
=== modified file 'doc/source/admin_guide.rst'
--- doc/source/admin_guide.rst 2011-03-31 22:32:41 +0000
+++ doc/source/admin_guide.rst 2011-06-09 21:09:39 +0000
@@ -222,22 +222,6 @@
222 Sample represents 1.00% of the object partition space222 Sample represents 1.00% of the object partition space
223223
224224
225------------------------------------
226Additional Cleanup Script for Swauth
227------------------------------------
228
229With Swauth, you'll want to install a cronjob to clean up any
230orphaned expired tokens. These orphaned tokens can occur when a "stampede"
231occurs where a single user authenticates several times concurrently. Generally,
232these orphaned tokens don't pose much of an issue, but it's good to clean them
233up once a "token life" period (default: 1 day or 86400 seconds).
234
235This should be as simple as adding `swauth-cleanup-tokens -A
236https://<PROXY_HOSTNAME>:8080/auth/ -K swauthkey > /dev/null` to a crontab
237entry on one of the proxies that is running Swauth; but run
238`swauth-cleanup-tokens` with no arguments for detailed help on the options
239available.
240
241------------------------225------------------------
242Debugging Tips and Tools226Debugging Tips and Tools
243------------------------227------------------------
244228
=== modified file 'doc/source/deployment_guide.rst'
--- doc/source/deployment_guide.rst 2011-01-25 00:28:22 +0000
+++ doc/source/deployment_guide.rst 2011-06-09 21:09:39 +0000
@@ -549,35 +549,17 @@
549 are even callable549 are even callable
550============================ =============== =============================550============================ =============== =============================
551551
552[auth]552[tempauth]
553
554============ =================================== ========================
555Option Default Description
556------------ ----------------------------------- ------------------------
557use Entry point for paste.deploy
558 to use for auth. To
559 use the swift dev auth,
560 set to:
561 `egg:swift#auth`
562ip 127.0.0.1 IP address of auth
563 server
564port 11000 Port of auth server
565ssl False If True, use SSL to
566 connect to auth
567node_timeout 10 Request timeout
568============ =================================== ========================
569
570[swauth]
571553
572===================== =============================== =======================554===================== =============================== =======================
573Option Default Description555Option Default Description
574--------------------- ------------------------------- -----------------------556--------------------- ------------------------------- -----------------------
575use Entry point for557use Entry point for
576 paste.deploy to use for558 paste.deploy to use for
577 auth. To use the swauth559 auth. To use tempauth
578 set to:560 set to:
579 `egg:swift#swauth`561 `egg:swift#tempauth`
580set log_name auth-server Label used when logging562set log_name tempauth Label used when logging
581set log_facility LOG_LOCAL0 Syslog log facility563set log_facility LOG_LOCAL0 Syslog log facility
582set log_level INFO Log level564set log_level INFO Log level
583set log_headers True If True, log headers in565set log_headers True If True, log headers in
@@ -593,16 +575,39 @@
593 reserves anything575 reserves anything
594 beginning with the576 beginning with the
595 letter `v`.577 letter `v`.
596default_swift_cluster local#http://127.0.0.1:8080/v1 The default Swift
597 cluster to place newly
598 created accounts on.
599token_life 86400 The number of seconds a578token_life 86400 The number of seconds a
600 token is valid.579 token is valid.
601node_timeout 10 Request timeout
602super_admin_key None The key for the
603 .super_admin account.
604===================== =============================== =======================580===================== =============================== =======================
605581
582Additionally, you need to list all the accounts/users you want here. The format
583is::
584
585 user_<account>_<user> = <key> [group] [group] [...] [storage_url]
586
587There are special groups of::
588
589 .reseller_admin = can do anything to any account for this auth
590 .admin = can do anything within the account
591
592If neither of these groups are specified, the user can only access containers
593that have been explicitly allowed for them by a .admin or .reseller_admin.
594
595The trailing optional storage_url allows you to specify an alternate url to
596hand back to the user upon authentication. If not specified, this defaults to::
597
598 http[s]://<ip>:<port>/v1/<reseller_prefix>_<account>
599
600Where http or https depends on whether cert_file is specified in the [DEFAULT]
601section, <ip> and <port> are based on the [DEFAULT] section's bind_ip and
602bind_port (falling back to 127.0.0.1 and 8080), <reseller_prefix> is from this
603section, and <account> is from the user_<account>_<user> name.
604
605Here are example entries, required for running the tests::
606
607 user_admin_admin = admin .admin .reseller_admin
608 user_test_tester = testing .admin
609 user_test2_tester2 = testing2 .admin
610 user_test_tester3 = testing3
606611
607------------------------612------------------------
608Memcached Considerations613Memcached Considerations
609614
=== modified file 'doc/source/development_auth.rst'
--- doc/source/development_auth.rst 2011-03-14 02:56:37 +0000
+++ doc/source/development_auth.rst 2011-06-09 21:09:39 +0000
@@ -6,7 +6,7 @@
6Creating Your Own Auth Server and Middleware6Creating Your Own Auth Server and Middleware
7--------------------------------------------7--------------------------------------------
88
9The included swift/common/middleware/swauth.py is a good example of how to9The included swift/common/middleware/tempauth.py is a good example of how to
10create an auth subsystem with proxy server auth middleware. The main points are10create an auth subsystem with proxy server auth middleware. The main points are
11that the auth middleware can reject requests up front, before they ever get to11that the auth middleware can reject requests up front, before they ever get to
12the Swift Proxy application, and afterwards when the proxy issues callbacks to12the Swift Proxy application, and afterwards when the proxy issues callbacks to
@@ -27,7 +27,7 @@
27environ['REMOTE_USER'] set to the authenticated user string but often more27environ['REMOTE_USER'] set to the authenticated user string but often more
28information is needed than just that.28information is needed than just that.
2929
30The included Swauth will set the REMOTE_USER to a comma separated list of30The included TempAuth will set the REMOTE_USER to a comma separated list of
31groups the user belongs to. The first group will be the "user's group", a group31groups the user belongs to. The first group will be the "user's group", a group
32that only the user belongs to. The second group will be the "account's group",32that only the user belongs to. The second group will be the "account's group",
33a group that includes all users for that auth account (different than the33a group that includes all users for that auth account (different than the
@@ -37,7 +37,7 @@
3737
38It is highly recommended that authentication server implementers prefix their38It is highly recommended that authentication server implementers prefix their
39tokens and Swift storage accounts they create with a configurable reseller39tokens and Swift storage accounts they create with a configurable reseller
40prefix (`AUTH_` by default with the included Swauth). This prefix will avoid40prefix (`AUTH_` by default with the included TempAuth). This prefix will avoid
41conflicts with other authentication servers that might be using the same41conflicts with other authentication servers that might be using the same
42Swift cluster. Otherwise, the Swift cluster will have to try all the resellers42Swift cluster. Otherwise, the Swift cluster will have to try all the resellers
43until one validates a token or all fail.43until one validates a token or all fail.
@@ -46,14 +46,14 @@
46'.' as that is reserved for internal Swift use (such as the .r for referrer46'.' as that is reserved for internal Swift use (such as the .r for referrer
47designations as you'll see later).47designations as you'll see later).
4848
49Example Authentication with Swauth:49Example Authentication with TempAuth:
5050
51 * Token AUTH_tkabcd is given to the Swauth middleware in a request's51 * Token AUTH_tkabcd is given to the TempAuth middleware in a request's
52 X-Auth-Token header.52 X-Auth-Token header.
53 * The Swauth middleware validates the token AUTH_tkabcd and discovers53 * The TempAuth middleware validates the token AUTH_tkabcd and discovers
54 it matches the "tester" user within the "test" account for the storage54 it matches the "tester" user within the "test" account for the storage
55 account "AUTH_storage_xyz".55 account "AUTH_storage_xyz".
56 * The Swauth server sets the REMOTE_USER to56 * The TempAuth middleware sets the REMOTE_USER to
57 "test:tester,test,AUTH_storage_xyz"57 "test:tester,test,AUTH_storage_xyz"
58 * Now this user will have full access (via authorization procedures later)58 * Now this user will have full access (via authorization procedures later)
59 to the AUTH_storage_xyz Swift storage account and access to containers in59 to the AUTH_storage_xyz Swift storage account and access to containers in
6060
=== modified file 'doc/source/development_saio.rst'
--- doc/source/development_saio.rst 2011-05-18 15:26:52 +0000
+++ doc/source/development_saio.rst 2011-06-09 21:09:39 +0000
@@ -265,16 +265,18 @@
265 log_facility = LOG_LOCAL1265 log_facility = LOG_LOCAL1
266266
267 [pipeline:main]267 [pipeline:main]
268 pipeline = healthcheck cache swauth proxy-server268 pipeline = healthcheck cache tempauth proxy-server
269 269
270 [app:proxy-server]270 [app:proxy-server]
271 use = egg:swift#proxy271 use = egg:swift#proxy
272 allow_account_management = true272 allow_account_management = true
273273
274 [filter:swauth]274 [filter:tempauth]
275 use = egg:swift#swauth275 use = egg:swift#tempauth
276 # Highly recommended to change this.276 user_admin_admin = admin .admin .reseller_admin
277 super_admin_key = swauthkey277 user_test_tester = testing .admin
278 user_test2_tester2 = testing2 .admin
279 user_test_tester3 = testing3
278280
279 [filter:healthcheck]281 [filter:healthcheck]
280 use = egg:swift#healthcheck282 use = egg:swift#healthcheck
@@ -558,8 +560,10 @@
558------------------------------------560------------------------------------
559561
560 #. Create `~/bin/resetswift.` 562 #. Create `~/bin/resetswift.`
561 If you are using a loopback device substitute `/dev/sdb1` with `/srv/swift-disk`.563
562 If you did not set up rsyslog for individual logging, remove the `find /var/log/swift...` line::564 If you are using a loopback device substitute `/dev/sdb1` with `/srv/swift-disk`.
565
566 If you did not set up rsyslog for individual logging, remove the `find /var/log/swift...` line::
563 567
564 #!/bin/bash568 #!/bin/bash
565569
@@ -608,18 +612,6 @@
608612
609 swift-init main start613 swift-init main start
610614
611 #. Create `~/bin/recreateaccounts`::
612
613 #!/bin/bash
614
615 # Replace swauthkey with whatever your super_admin key is (recorded in
616 # /etc/swift/proxy-server.conf).
617 swauth-prep -K swauthkey
618 swauth-add-user -K swauthkey -a test tester testing
619 swauth-add-user -K swauthkey -a test2 tester2 testing2
620 swauth-add-user -K swauthkey test tester3 testing3
621 swauth-add-user -K swauthkey -a -r reseller reseller reseller
622
623 #. Create `~/bin/startrest`::615 #. Create `~/bin/startrest`::
624616
625 #!/bin/bash617 #!/bin/bash
626618
=== modified file 'doc/source/howto_installmultinode.rst'
--- doc/source/howto_installmultinode.rst 2011-05-17 03:59:57 +0000
+++ doc/source/howto_installmultinode.rst 2011-06-09 21:09:39 +0000
@@ -13,7 +13,7 @@
13Basic architecture and terms13Basic architecture and terms
14----------------------------14----------------------------
15- *node* - a host machine running one or more Swift services15- *node* - a host machine running one or more Swift services
16- *Proxy node* - node that runs Proxy services; also runs Swauth16- *Proxy node* - node that runs Proxy services; also runs TempAuth
17- *Storage node* - node that runs Account, Container, and Object services17- *Storage node* - node that runs Account, Container, and Object services
18- *ring* - a set of mappings of Swift data to physical devices18- *ring* - a set of mappings of Swift data to physical devices
1919
@@ -23,7 +23,7 @@
2323
24 - Runs the swift-proxy-server processes which proxy requests to the24 - Runs the swift-proxy-server processes which proxy requests to the
25 appropriate Storage nodes. The proxy server will also contain25 appropriate Storage nodes. The proxy server will also contain
26 the Swauth service as WSGI middleware.26 the TempAuth service as WSGI middleware.
2727
28- five Storage nodes28- five Storage nodes
2929
@@ -130,17 +130,15 @@
130 user = swift130 user = swift
131 131
132 [pipeline:main]132 [pipeline:main]
133 pipeline = healthcheck cache swauth proxy-server133 pipeline = healthcheck cache tempauth proxy-server
134 134
135 [app:proxy-server]135 [app:proxy-server]
136 use = egg:swift#proxy136 use = egg:swift#proxy
137 allow_account_management = true137 allow_account_management = true
138 138
139 [filter:swauth]139 [filter:tempauth]
140 use = egg:swift#swauth140 use = egg:swift#tempauth
141 default_swift_cluster = local#https://$PROXY_LOCAL_NET_IP:8080/v1141 user_system_root = testpass .admin https://$PROXY_LOCAL_NET_IP:8080/v1/AUTH_system
142 # Highly recommended to change this key to something else!
143 super_admin_key = swauthkey
144 142
145 [filter:healthcheck]143 [filter:healthcheck]
146 use = egg:swift#healthcheck144 use = egg:swift#healthcheck
@@ -366,16 +364,6 @@
366364
367You run these commands from the Proxy node.365You run these commands from the Proxy node.
368366
369#. Create a user with administrative privileges (account = system,
370 username = root, password = testpass). Make sure to replace
371 ``swauthkey`` with whatever super_admin key you assigned in
372 the proxy-server.conf file
373 above. *Note: None of the values of
374 account, username, or password are special - they can be anything.*::
375
376 swauth-prep -A https://$PROXY_LOCAL_NET_IP:8080/auth/ -K swauthkey
377 swauth-add-user -A https://$PROXY_LOCAL_NET_IP:8080/auth/ -K swauthkey -a system root testpass
378
379#. Get an X-Storage-Url and X-Auth-Token::367#. Get an X-Storage-Url and X-Auth-Token::
380368
381 curl -k -v -H 'X-Storage-User: system:root' -H 'X-Storage-Pass: testpass' https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0369 curl -k -v -H 'X-Storage-User: system:root' -H 'X-Storage-Pass: testpass' https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0
@@ -430,45 +418,16 @@
430 use = egg:swift#memcache418 use = egg:swift#memcache
431 memcache_servers = $PROXY_LOCAL_NET_IP:11211419 memcache_servers = $PROXY_LOCAL_NET_IP:11211
432420
433#. Change the default_cluster_url to point to the load balanced url, rather than the first proxy server you created in /etc/swift/proxy-server.conf::421#. Change the storage url for any users to point to the load balanced url, rather than the first proxy server you created in /etc/swift/proxy-server.conf::
434422
435 [filter:swauth]423 [filter:tempauth]
436 use = egg:swift#swauth424 use = egg:swift#tempauth
437 default_swift_cluster = local#http://<LOAD_BALANCER_HOSTNAME>/v1425 user_system_root = testpass .admin http[s]://<LOAD_BALANCER_HOSTNAME>:<PORT>/v1/AUTH_system
438 # Highly recommended to change this key to something else!
439 super_admin_key = swauthkey
440
441#. The above will make new accounts with the new default_swift_cluster URL, however it won't change any existing accounts. You can change a service URL for existing accounts with::
442
443 First retreve what the URL was::
444
445 swauth-list -A https://$PROXY_LOCAL_NET_IP:8080/auth/ -K swauthkey <account>
446
447 And then update it with::
448
449 swauth-set-account-service -A https://$PROXY_LOCAL_NET_IP:8080/auth/ -K swauthkey <account> storage local <new_url_for_the_account>
450
451 Make the <new_url_for_the_account> look just like it's original URL but with the host:port update you want.
452426
453#. 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. 427#. 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.
454428
455#. 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. 429#. 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.
456430
457Additional Cleanup Script for Swauth
458------------------------------------
459
460With Swauth, you'll want to install a cronjob to clean up any
461orphaned expired tokens. These orphaned tokens can occur when a "stampede"
462occurs where a single user authenticates several times concurrently. Generally,
463these orphaned tokens don't pose much of an issue, but it's good to clean them
464up once a "token life" period (default: 1 day or 86400 seconds).
465
466This should be as simple as adding `swauth-cleanup-tokens -A
467https://<PROXY_HOSTNAME>:8080/auth/ -K swauthkey > /dev/null` to a crontab
468entry on one of the proxies that is running Swauth; but run
469`swauth-cleanup-tokens` with no arguments for detailed help on the options
470available.
471
472Troubleshooting Notes431Troubleshooting Notes
473---------------------432---------------------
474If you see problems, look in var/log/syslog (or messages on some distros). 433If you see problems, look in var/log/syslog (or messages on some distros).
475434
=== modified file 'doc/source/misc.rst'
--- doc/source/misc.rst 2011-03-24 03:37:07 +0000
+++ doc/source/misc.rst 2011-06-09 21:09:39 +0000
@@ -33,12 +33,12 @@
33 :members:33 :members:
34 :show-inheritance:34 :show-inheritance:
3535
36.. _common_swauth:36.. _common_tempauth:
3737
38Swauth38TempAuth
39======39========
4040
41.. automodule:: swift.common.middleware.swauth41.. automodule:: swift.common.middleware.tempauth
42 :members:42 :members:
43 :show-inheritance:43 :show-inheritance:
4444
4545
=== modified file 'doc/source/overview_auth.rst'
--- doc/source/overview_auth.rst 2011-03-14 02:56:37 +0000
+++ doc/source/overview_auth.rst 2011-06-09 21:09:39 +0000
@@ -2,9 +2,9 @@
2The Auth System2The Auth System
3===============3===============
44
5------5--------
6Swauth6TempAuth
7------7--------
88
9The auth system for Swift is loosely based on the auth system from the existing9The auth system for Swift is loosely based on the auth system from the existing
10Rackspace architecture -- actually from a few existing auth systems -- and is10Rackspace architecture -- actually from a few existing auth systems -- and is
@@ -27,7 +27,7 @@
27Swift will make calls to the auth system, giving the auth token to be27Swift will make calls to the auth system, giving the auth token to be
28validated. For a valid token, the auth system responds with an overall28validated. For a valid token, the auth system responds with an overall
29expiration in seconds from now. Swift will cache the token up to the expiration29expiration in seconds from now. Swift will cache the token up to the expiration
30time. The included Swauth also has the concept of admin and non-admin users30time. The included TempAuth also has the concept of admin and non-admin users
31within an account. Admin users can do anything within the account. Non-admin31within an account. Admin users can do anything within the account. Non-admin
32users can only perform operations per container based on the container's32users can only perform operations per container based on the container's
33X-Container-Read and X-Container-Write ACLs. For more information on ACLs, see33X-Container-Read and X-Container-Write ACLs. For more information on ACLs, see
@@ -40,152 +40,9 @@
40Extending Auth40Extending Auth
41--------------41--------------
4242
43Swauth is written as wsgi middleware, so implementing your own auth is as easy43TempAuth is written as wsgi middleware, so implementing your own auth is as
44as writing new wsgi middleware, and plugging it in to the proxy server.44easy as writing new wsgi middleware, and plugging it in to the proxy server.
45The KeyStone project and the Swauth project are examples of additional auth
46services.
4547
46Also, see :doc:`development_auth`.48Also, see :doc:`development_auth`.
47
48
49--------------
50Swauth Details
51--------------
52
53The Swauth system is included at swift/common/middleware/swauth.py; a scalable
54authentication and authorization system that uses Swift itself as its backing
55store. This section will describe how it stores its data.
56
57At the topmost level, the auth system has its own Swift account it stores its
58own account information within. This Swift account is known as
59self.auth_account in the code and its name is in the format
60self.reseller_prefix + ".auth". In this text, we'll refer to this account as
61<auth_account>.
62
63The containers whose names do not begin with a period represent the accounts
64within the auth service. For example, the <auth_account>/test container would
65represent the "test" account.
66
67The objects within each container represent the users for that auth service
68account. For example, the <auth_account>/test/bob object would represent the
69user "bob" within the auth service account of "test". Each of these user
70objects contain a JSON dictionary of the format::
71
72 {"auth": "<auth_type>:<auth_value>", "groups": <groups_array>}
73
74The `<auth_type>` can only be `plaintext` at this time, and the `<auth_value>`
75is the plain text password itself.
76
77The `<groups_array>` contains at least two groups. The first is a unique group
78identifying that user and it's name is of the format `<user>:<account>`. The
79second group is the `<account>` itself. Additional groups of `.admin` for
80account administrators and `.reseller_admin` for reseller administrators may
81exist. Here's an example user JSON dictionary::
82
83 {"auth": "plaintext:testing",
84 "groups": ["name": "test:tester", "name": "test", "name": ".admin"]}
85
86To map an auth service account to a Swift storage account, the Service Account
87Id string is stored in the `X-Container-Meta-Account-Id` header for the
88<auth_account>/<account> container. To map back the other way, an
89<auth_account>/.account_id/<account_id> object is created with the contents of
90the corresponding auth service's account name.
91
92Also, to support a future where the auth service will support multiple Swift
93clusters or even multiple services for the same auth service account, an
94<auth_account>/<account>/.services object is created with its contents having a
95JSON dictionary of the format::
96
97 {"storage": {"default": "local", "local": <url>}}
98
99The "default" is always "local" right now, and "local" is always the single
100Swift cluster URL; but in the future there can be more than one cluster with
101various names instead of just "local", and the "default" key's value will
102contain the primary cluster to use for that account. Also, there may be more
103services in addition to the current "storage" service right now.
104
105Here's an example .services dictionary at the moment::
106
107 {"storage":
108 {"default": "local",
109 "local": "http://127.0.0.1:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9"}}
110
111But, here's an example of what the dictionary may look like in the future::
112
113 {"storage":
114 {"default": "dfw",
115 "dfw": "http://dfw.storage.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9",
116 "ord": "http://ord.storage.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9",
117 "sat": "http://ord.storage.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9"},
118 "servers":
119 {"default": "dfw",
120 "dfw": "http://dfw.servers.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9",
121 "ord": "http://ord.servers.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9",
122 "sat": "http://ord.servers.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9"}}
123
124Lastly, the tokens themselves are stored as objects in the
125`<auth_account>/.token_[0-f]` containers. The names of the objects are the
126token strings themselves, such as `AUTH_tked86bbd01864458aa2bd746879438d5a`.
127The exact `.token_[0-f]` container chosen is based on the final digit of the
128token name, such as `.token_a` for the token
129`AUTH_tked86bbd01864458aa2bd746879438d5a`. The contents of the token objects
130are JSON dictionaries of the format::
131
132 {"account": <account>,
133 "user": <user>,
134 "account_id": <account_id>,
135 "groups": <groups_array>,
136 "expires": <time.time() value>}
137
138The `<account>` is the auth service account's name for that token. The `<user>`
139is the user within the account for that token. The `<account_id>` is the
140same as the `X-Container-Meta-Account-Id` for the auth service's account,
141as described above. The `<groups_array>` is the user's groups, as described
142above with the user object. The "expires" value indicates when the token is no
143longer valid, as compared to Python's time.time() value.
144
145Here's an example token object's JSON dictionary::
146
147 {"account": "test",
148 "user": "tester",
149 "account_id": "AUTH_8980f74b1cda41e483cbe0a925f448a9",
150 "groups": ["name": "test:tester", "name": "test", "name": ".admin"],
151 "expires": 1291273147.1624689}
152
153To easily map a user to an already issued token, the token name is stored in
154the user object's `X-Object-Meta-Auth-Token` header.
155
156Here is an example full listing of an <auth_account>::
157
158 .account_id
159 AUTH_2282f516-559f-4966-b239-b5c88829e927
160 AUTH_f6f57a3c-33b5-4e85-95a5-a801e67505c8
161 AUTH_fea96a36-c177-4ca4-8c7e-b8c715d9d37b
162 .token_0
163 .token_1
164 .token_2
165 .token_3
166 .token_4
167 .token_5
168 .token_6
169 AUTH_tk9d2941b13d524b268367116ef956dee6
170 .token_7
171 .token_8
172 AUTH_tk93627c6324c64f78be746f1e6a4e3f98
173 .token_9
174 .token_a
175 .token_b
176 .token_c
177 .token_d
178 .token_e
179 AUTH_tk0d37d286af2c43ffad06e99112b3ec4e
180 .token_f
181 AUTH_tk766bbde93771489982d8dc76979d11cf
182 reseller
183 .services
184 reseller
185 test
186 .services
187 tester
188 tester3
189 test2
190 .services
191 tester2
19249
=== modified file 'etc/proxy-server.conf-sample'
--- etc/proxy-server.conf-sample 2011-03-25 08:33:46 +0000
+++ etc/proxy-server.conf-sample 2011-06-09 21:09:39 +0000
@@ -13,7 +13,7 @@
13# log_level = INFO13# log_level = INFO
1414
15[pipeline:main]15[pipeline:main]
16pipeline = catch_errors healthcheck cache ratelimit swauth proxy-server16pipeline = catch_errors healthcheck cache ratelimit tempauth proxy-server
1717
18[app:proxy-server]18[app:proxy-server]
19use = egg:swift#proxy19use = egg:swift#proxy
@@ -41,10 +41,10 @@
41# 'false' no one, even authorized, can.41# 'false' no one, even authorized, can.
42# allow_account_management = false42# allow_account_management = false
4343
44[filter:swauth]44[filter:tempauth]
45use = egg:swift#swauth45use = egg:swift#tempauth
46# You can override the default log routing for this filter here:46# You can override the default log routing for this filter here:
47# set log_name = auth-server47# set log_name = tempauth
48# set log_facility = LOG_LOCAL048# set log_facility = LOG_LOCAL0
49# set log_level = INFO49# set log_level = INFO
50# set log_headers = False50# set log_headers = False
@@ -54,21 +54,28 @@
54# multiple auth systems are in use for one Swift cluster.54# multiple auth systems are in use for one Swift cluster.
55# reseller_prefix = AUTH55# reseller_prefix = AUTH
56# The auth prefix will cause requests beginning with this prefix to be routed56# The auth prefix will cause requests beginning with this prefix to be routed
57# to the auth subsystem, for granting tokens, creating accounts, users, etc.57# to the auth subsystem, for granting tokens, etc.
58# auth_prefix = /auth/58# auth_prefix = /auth/
59# Cluster strings are of the format name#url where name is a short name for the
60# Swift cluster and url is the url to the proxy server(s) for the cluster.
61# default_swift_cluster = local#http://127.0.0.1:8080/v1
62# You may also use the format name#url#url where the first url is the one
63# given to users to access their account (public url) and the second is the one
64# used by swauth itself to create and delete accounts (private url). This is
65# useful when a load balancer url should be used by users, but swauth itself is
66# behind the load balancer. Example:
67# default_swift_cluster = local#https://public.com:8080/v1#http://private.com:8080/v1
68# token_life = 8640059# token_life = 86400
69# node_timeout = 1060# Lastly, you need to list all the accounts/users you want here. The format is:
70# Highly recommended to change this.61# user_<account>_<user> = <key> [group] [group] [...] [storage_url]
71super_admin_key = swauthkey62# There are special groups of:
63# .reseller_admin = can do anything to any account for this auth
64# .admin = can do anything within the account
65# If neither of these groups are specified, the user can only access containers
66# that have been explicitly allowed for them by a .admin or .reseller_admin.
67# The trailing optional storage_url allows you to specify an alternate url to
68# hand back to the user upon authentication. If not specified, this defaults to
69# http[s]://<ip>:<port>/v1/<reseller_prefix>_<account> where http or https
70# depends on whether cert_file is specified in the [DEFAULT] section, <ip> and
71# <port> are based on the [DEFAULT] section's bind_ip and bind_port (falling
72# back to 127.0.0.1 and 8080), <reseller_prefix> is from this section, and
73# <account> is from the user_<account>_<user> name.
74# Here are example entries, required for running the tests:
75user_admin_admin = admin .admin .reseller_admin
76user_test_tester = testing .admin
77user_test2_tester2 = testing2 .admin
78user_test_tester3 = testing3
7279
73[filter:healthcheck]80[filter:healthcheck]
74use = egg:swift#healthcheck81use = egg:swift#healthcheck
7582
=== modified file 'setup.py'
--- setup.py 2011-05-26 09:25:39 +0000
+++ setup.py 2011-06-09 21:09:39 +0000
@@ -96,10 +96,6 @@
96 'bin/swift-log-stats-collector',96 'bin/swift-log-stats-collector',
97 'bin/swift-account-stats-logger',97 'bin/swift-account-stats-logger',
98 'bin/swift-container-stats-logger',98 'bin/swift-container-stats-logger',
99 'bin/swauth-add-account', 'bin/swauth-add-user',
100 'bin/swauth-cleanup-tokens', 'bin/swauth-delete-account',
101 'bin/swauth-delete-user', 'bin/swauth-list', 'bin/swauth-prep',
102 'bin/swauth-set-account-service',
103 ],99 ],
104 entry_points={100 entry_points={
105 'paste.app_factory': [101 'paste.app_factory': [
@@ -109,7 +105,6 @@
109 'account=swift.account.server:app_factory',105 'account=swift.account.server:app_factory',
110 ],106 ],
111 'paste.filter_factory': [107 'paste.filter_factory': [
112 'swauth=swift.common.middleware.swauth:filter_factory',
113 'healthcheck=swift.common.middleware.healthcheck:filter_factory',108 'healthcheck=swift.common.middleware.healthcheck:filter_factory',
114 'memcache=swift.common.middleware.memcache:filter_factory',109 'memcache=swift.common.middleware.memcache:filter_factory',
115 'ratelimit=swift.common.middleware.ratelimit:filter_factory',110 'ratelimit=swift.common.middleware.ratelimit:filter_factory',
@@ -118,6 +113,7 @@
118 'domain_remap=swift.common.middleware.domain_remap:filter_factory',113 'domain_remap=swift.common.middleware.domain_remap:filter_factory',
119 'swift3=swift.common.middleware.swift3:filter_factory',114 'swift3=swift.common.middleware.swift3:filter_factory',
120 'staticweb=swift.common.middleware.staticweb:filter_factory',115 'staticweb=swift.common.middleware.staticweb:filter_factory',
116 'tempauth=swift.common.middleware.tempauth:filter_factory',
121 ],117 ],
122 },118 },
123 )119 )
124120
=== modified file 'swift/common/middleware/staticweb.py'
--- swift/common/middleware/staticweb.py 2011-03-25 19:21:35 +0000
+++ swift/common/middleware/staticweb.py 2011-06-09 21:09:39 +0000
@@ -28,7 +28,7 @@
28 ...28 ...
2929
30 [pipeline:main]30 [pipeline:main]
31 pipeline = healthcheck cache swauth staticweb proxy-server31 pipeline = healthcheck cache tempauth staticweb proxy-server
3232
33 ...33 ...
3434
3535
=== removed file 'swift/common/middleware/swauth.py'
--- swift/common/middleware/swauth.py 2011-05-09 20:21:34 +0000
+++ swift/common/middleware/swauth.py 1970-01-01 00:00:00 +0000
@@ -1,1374 +0,0 @@
1# Copyright (c) 2010 OpenStack, LLC.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12# implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16try:
17 import simplejson as json
18except ImportError:
19 import json
20from httplib import HTTPConnection, HTTPSConnection
21from time import gmtime, strftime, time
22from traceback import format_exc
23from urllib import quote, unquote
24from uuid import uuid4
25from hashlib import md5, sha1
26import hmac
27import base64
28
29from eventlet.timeout import Timeout
30from eventlet import TimeoutError
31from webob import Response, Request
32from webob.exc import HTTPAccepted, HTTPBadRequest, HTTPConflict, \
33 HTTPCreated, HTTPForbidden, HTTPNoContent, HTTPNotFound, \
34 HTTPServiceUnavailable, HTTPUnauthorized
35
36from swift.common.bufferedhttp import http_connect_raw as http_connect
37from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
38from swift.common.utils import cache_from_env, get_logger, split_path, urlparse
39
40
41class Swauth(object):
42 """
43 Scalable authentication and authorization system that uses Swift as its
44 backing store.
45
46 :param app: The next WSGI app in the pipeline
47 :param conf: The dict of configuration values
48 """
49
50 def __init__(self, app, conf):
51 self.app = app
52 self.conf = conf
53 self.logger = get_logger(conf, log_route='swauth')
54 self.log_headers = conf.get('log_headers') == 'True'
55 self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip()
56 if self.reseller_prefix and self.reseller_prefix[-1] != '_':
57 self.reseller_prefix += '_'
58 self.auth_prefix = conf.get('auth_prefix', '/auth/')
59 if not self.auth_prefix:
60 self.auth_prefix = '/auth/'
61 if self.auth_prefix[0] != '/':
62 self.auth_prefix = '/' + self.auth_prefix
63 if self.auth_prefix[-1] != '/':
64 self.auth_prefix += '/'
65 self.auth_account = '%s.auth' % self.reseller_prefix
66 self.default_swift_cluster = conf.get('default_swift_cluster',
67 'local#http://127.0.0.1:8080/v1')
68 # This setting is a little messy because of the options it has to
69 # provide. The basic format is cluster_name#url, such as the default
70 # value of local#http://127.0.0.1:8080/v1.
71 # If the URL given to the user needs to differ from the url used by
72 # Swauth to create/delete accounts, there's a more complex format:
73 # cluster_name#url#url, such as
74 # local#https://public.com:8080/v1#http://private.com:8080/v1.
75 cluster_parts = self.default_swift_cluster.split('#', 2)
76 self.dsc_name = cluster_parts[0]
77 if len(cluster_parts) == 3:
78 self.dsc_url = cluster_parts[1].rstrip('/')
79 self.dsc_url2 = cluster_parts[2].rstrip('/')
80 elif len(cluster_parts) == 2:
81 self.dsc_url = self.dsc_url2 = cluster_parts[1].rstrip('/')
82 else:
83 raise Exception('Invalid cluster format')
84 self.dsc_parsed = urlparse(self.dsc_url)
85 if self.dsc_parsed.scheme not in ('http', 'https'):
86 raise Exception('Cannot handle protocol scheme %s for url %s' %
87 (self.dsc_parsed.scheme, repr(self.dsc_url)))
88 self.dsc_parsed2 = urlparse(self.dsc_url2)
89 if self.dsc_parsed2.scheme not in ('http', 'https'):
90 raise Exception('Cannot handle protocol scheme %s for url %s' %
91 (self.dsc_parsed2.scheme, repr(self.dsc_url2)))
92 self.super_admin_key = conf.get('super_admin_key')
93 if not self.super_admin_key:
94 msg = _('No super_admin_key set in conf file! Exiting.')
95 try:
96 self.logger.critical(msg)
97 except Exception:
98 pass
99 raise ValueError(msg)
100 self.token_life = int(conf.get('token_life', 86400))
101 self.timeout = int(conf.get('node_timeout', 10))
102 self.itoken = None
103 self.itoken_expires = None
104
105 def __call__(self, env, start_response):
106 """
107 Accepts a standard WSGI application call, authenticating the request
108 and installing callback hooks for authorization and ACL header
109 validation. For an authenticated request, REMOTE_USER will be set to a
110 comma separated list of the user's groups.
111
112 With a non-empty reseller prefix, acts as the definitive auth service
113 for just tokens and accounts that begin with that prefix, but will deny
114 requests outside this prefix if no other auth middleware overrides it.
115
116 With an empty reseller prefix, acts as the definitive auth service only
117 for tokens that validate to a non-empty set of groups. For all other
118 requests, acts as the fallback auth service when no other auth
119 middleware overrides it.
120
121 Alternatively, if the request matches the self.auth_prefix, the request
122 will be routed through the internal auth request handler (self.handle).
123 This is to handle creating users, accounts, granting tokens, etc.
124 """
125 if 'HTTP_X_CF_TRANS_ID' not in env:
126 env['HTTP_X_CF_TRANS_ID'] = 'tx' + str(uuid4())
127 if env.get('PATH_INFO', '').startswith(self.auth_prefix):
128 return self.handle(env, start_response)
129 s3 = env.get('HTTP_AUTHORIZATION')
130 token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN'))
131 if s3 or (token and token.startswith(self.reseller_prefix)):
132 # Note: Empty reseller_prefix will match all tokens.
133 groups = self.get_groups(env, token)
134 if groups:
135 env['REMOTE_USER'] = groups
136 user = groups and groups.split(',', 1)[0] or ''
137 # We know the proxy logs the token, so we augment it just a bit
138 # to also log the authenticated user.
139 env['HTTP_X_AUTH_TOKEN'] = \
140 '%s,%s' % (user, 's3' if s3 else token)
141 env['swift.authorize'] = self.authorize
142 env['swift.clean_acl'] = clean_acl
143 else:
144 # Unauthorized token
145 if self.reseller_prefix:
146 # Because I know I'm the definitive auth for this token, I
147 # can deny it outright.
148 return HTTPUnauthorized()(env, start_response)
149 # Because I'm not certain if I'm the definitive auth for empty
150 # reseller_prefixed tokens, I won't overwrite swift.authorize.
151 elif 'swift.authorize' not in env:
152 env['swift.authorize'] = self.denied_response
153 else:
154 if self.reseller_prefix:
155 # With a non-empty reseller_prefix, I would like to be called
156 # back for anonymous access to accounts I know I'm the
157 # definitive auth for.
158 try:
159 version, rest = split_path(env.get('PATH_INFO', ''),
160 1, 2, True)
161 except ValueError:
162 return HTTPNotFound()(env, start_response)
163 if rest and rest.startswith(self.reseller_prefix):
164 # Handle anonymous access to accounts I'm the definitive
165 # auth for.
166 env['swift.authorize'] = self.authorize
167 env['swift.clean_acl'] = clean_acl
168 # Not my token, not my account, I can't authorize this request,
169 # deny all is a good idea if not already set...
170 elif 'swift.authorize' not in env:
171 env['swift.authorize'] = self.denied_response
172 # Because I'm not certain if I'm the definitive auth for empty
173 # reseller_prefixed accounts, I won't overwrite swift.authorize.
174 elif 'swift.authorize' not in env:
175 env['swift.authorize'] = self.authorize
176 env['swift.clean_acl'] = clean_acl
177 return self.app(env, start_response)
178
179 def get_groups(self, env, token):
180 """
181 Get groups for the given token.
182
183 :param env: The current WSGI environment dictionary.
184 :param token: Token to validate and return a group string for.
185
186 :returns: None if the token is invalid or a string containing a comma
187 separated list of groups the authenticated user is a member
188 of. The first group in the list is also considered a unique
189 identifier for that user.
190 """
191 groups = None
192 memcache_client = cache_from_env(env)
193 if memcache_client:
194 memcache_key = '%s/auth/%s' % (self.reseller_prefix, token)
195 cached_auth_data = memcache_client.get(memcache_key)
196 if cached_auth_data:
197 expires, groups = cached_auth_data
198 if expires < time():
199 groups = None
200
201 if env.get('HTTP_AUTHORIZATION'):
202 account = env['HTTP_AUTHORIZATION'].split(' ')[1]
203 account, user, sign = account.split(':')
204 path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user))
205 resp = self.make_request(env, 'GET', path).get_response(self.app)
206 if resp.status_int // 100 != 2:
207 return None
208
209 if 'x-object-meta-account-id' in resp.headers:
210 account_id = resp.headers['x-object-meta-account-id']
211 else:
212 path = quote('/v1/%s/%s' % (self.auth_account, account))
213 resp2 = self.make_request(env, 'HEAD',
214 path).get_response(self.app)
215 if resp2.status_int // 100 != 2:
216 return None
217 account_id = resp2.headers['x-container-meta-account-id']
218
219 path = env['PATH_INFO']
220 env['PATH_INFO'] = path.replace("%s:%s" % (account, user),
221 account_id, 1)
222 detail = json.loads(resp.body)
223
224 password = detail['auth'].split(':')[-1]
225 msg = base64.urlsafe_b64decode(unquote(token))
226 s = base64.encodestring(hmac.new(detail['auth'].split(':')[-1],
227 msg, sha1).digest()).strip()
228 if s != sign:
229 return None
230 groups = [g['name'] for g in detail['groups']]
231 if '.admin' in groups:
232 groups.remove('.admin')
233 groups.append(account_id)
234 groups = ','.join(groups)
235 return groups
236
237 if not groups:
238 path = quote('/v1/%s/.token_%s/%s' %
239 (self.auth_account, token[-1], token))
240 resp = self.make_request(env, 'GET', path).get_response(self.app)
241 if resp.status_int // 100 != 2:
242 return None
243 detail = json.loads(resp.body)
244 if detail['expires'] < time():
245 self.make_request(env, 'DELETE', path).get_response(self.app)
246 return None
247 groups = [g['name'] for g in detail['groups']]
248 if '.admin' in groups:
249 groups.remove('.admin')
250 groups.append(detail['account_id'])
251 groups = ','.join(groups)
252 if memcache_client:
253 memcache_client.set(memcache_key, (detail['expires'], groups),
254 timeout=float(detail['expires'] - time()))
255 return groups
256
257 def authorize(self, req):
258 """
259 Returns None if the request is authorized to continue or a standard
260 WSGI response callable if not.
261 """
262 try:
263 version, account, container, obj = split_path(req.path, 1, 4, True)
264 except ValueError:
265 return HTTPNotFound(request=req)
266 if not account or not account.startswith(self.reseller_prefix):
267 return self.denied_response(req)
268 user_groups = (req.remote_user or '').split(',')
269 if '.reseller_admin' in user_groups and \
270 account != self.reseller_prefix and \
271 account[len(self.reseller_prefix)] != '.':
272 return None
273 if account in user_groups and \
274 (req.method not in ('DELETE', 'PUT') or container):
275 # If the user is admin for the account and is not trying to do an
276 # account DELETE or PUT...
277 return None
278 referrers, groups = parse_acl(getattr(req, 'acl', None))
279 if referrer_allowed(req.referer, referrers):
280 if obj or '.rlistings' in groups:
281 return None
282 return self.denied_response(req)
283 if not req.remote_user:
284 return self.denied_response(req)
285 for user_group in user_groups:
286 if user_group in groups:
287 return None
288 return self.denied_response(req)
289
290 def denied_response(self, req):
291 """
292 Returns a standard WSGI response callable with the status of 403 or 401
293 depending on whether the REMOTE_USER is set or not.
294 """
295 if req.remote_user:
296 return HTTPForbidden(request=req)
297 else:
298 return HTTPUnauthorized(request=req)
299
300 def handle(self, env, start_response):
301 """
302 WSGI entry point for auth requests (ones that match the
303 self.auth_prefix).
304 Wraps env in webob.Request object and passes it down.
305
306 :param env: WSGI environment dictionary
307 :param start_response: WSGI callable
308 """
309 try:
310 req = Request(env)
311 if self.auth_prefix:
312 req.path_info_pop()
313 req.bytes_transferred = '-'
314 req.client_disconnect = False
315 if 'x-storage-token' in req.headers and \
316 'x-auth-token' not in req.headers:
317 req.headers['x-auth-token'] = req.headers['x-storage-token']
318 if 'eventlet.posthooks' in env:
319 env['eventlet.posthooks'].append(
320 (self.posthooklogger, (req,), {}))
321 return self.handle_request(req)(env, start_response)
322 else:
323 # Lack of posthook support means that we have to log on the
324 # start of the response, rather than after all the data has
325 # been sent. This prevents logging client disconnects
326 # differently than full transmissions.
327 response = self.handle_request(req)(env, start_response)
328 self.posthooklogger(env, req)
329 return response
330 except (Exception, TimeoutError):
331 print "EXCEPTION IN handle: %s: %s" % (format_exc(), env)
332 start_response('500 Server Error',
333 [('Content-Type', 'text/plain')])
334 return ['Internal server error.\n']
335
336 def handle_request(self, req):
337 """
338 Entry point for auth requests (ones that match the self.auth_prefix).
339 Should return a WSGI-style callable (such as webob.Response).
340
341 :param req: webob.Request object
342 """
343 req.start_time = time()
344 handler = None
345 try:
346 version, account, user, _junk = split_path(req.path_info,
347 minsegs=1, maxsegs=4, rest_with_last=True)
348 except ValueError:
349 return HTTPNotFound(request=req)
350 if version in ('v1', 'v1.0', 'auth'):
351 if req.method == 'GET':
352 handler = self.handle_get_token
353 elif version == 'v2':
354 req.path_info_pop()
355 if req.method == 'GET':
356 if not account and not user:
357 handler = self.handle_get_reseller
358 elif account:
359 if not user:
360 handler = self.handle_get_account
361 elif account == '.token':
362 req.path_info_pop()
363 handler = self.handle_validate_token
364 else:
365 handler = self.handle_get_user
366 elif req.method == 'PUT':
367 if not user:
368 handler = self.handle_put_account
369 else:
370 handler = self.handle_put_user
371 elif req.method == 'DELETE':
372 if not user:
373 handler = self.handle_delete_account
374 else:
375 handler = self.handle_delete_user
376 elif req.method == 'POST':
377 if account == '.prep':
378 handler = self.handle_prep
379 elif user == '.services':
380 handler = self.handle_set_services
381 if not handler:
382 req.response = HTTPBadRequest(request=req)
383 else:
384 req.response = handler(req)
385 return req.response
386
387 def handle_prep(self, req):
388 """
389 Handles the POST v2/.prep call for preparing the backing store Swift
390 cluster for use with the auth subsystem. Can only be called by
391 .super_admin.
392
393 :param req: The webob.Request to process.
394 :returns: webob.Response, 204 on success
395 """
396 if not self.is_super_admin(req):
397 return HTTPForbidden(request=req)
398 path = quote('/v1/%s' % self.auth_account)
399 resp = self.make_request(req.environ, 'PUT',
400 path).get_response(self.app)
401 if resp.status_int // 100 != 2:
402 raise Exception('Could not create the main auth account: %s %s' %
403 (path, resp.status))
404 path = quote('/v1/%s/.account_id' % self.auth_account)
405 resp = self.make_request(req.environ, 'PUT',
406 path).get_response(self.app)
407 if resp.status_int // 100 != 2:
408 raise Exception('Could not create container: %s %s' %
409 (path, resp.status))
410 for container in xrange(16):
411 path = quote('/v1/%s/.token_%x' % (self.auth_account, container))
412 resp = self.make_request(req.environ, 'PUT',
413 path).get_response(self.app)
414 if resp.status_int // 100 != 2:
415 raise Exception('Could not create container: %s %s' %
416 (path, resp.status))
417 return HTTPNoContent(request=req)
418
419 def handle_get_reseller(self, req):
420 """
421 Handles the GET v2 call for getting general reseller information
422 (currently just a list of accounts). Can only be called by a
423 .reseller_admin.
424
425 On success, a JSON dictionary will be returned with a single `accounts`
426 key whose value is list of dicts. Each dict represents an account and
427 currently only contains the single key `name`. For example::
428
429 {"accounts": [{"name": "reseller"}, {"name": "test"},
430 {"name": "test2"}]}
431
432 :param req: The webob.Request to process.
433 :returns: webob.Response, 2xx on success with a JSON dictionary as
434 explained above.
435 """
436 if not self.is_reseller_admin(req):
437 return HTTPForbidden(request=req)
438 listing = []
439 marker = ''
440 while True:
441 path = '/v1/%s?format=json&marker=%s' % (quote(self.auth_account),
442 quote(marker))
443 resp = self.make_request(req.environ, 'GET',
444 path).get_response(self.app)
445 if resp.status_int // 100 != 2:
446 raise Exception('Could not list main auth account: %s %s' %
447 (path, resp.status))
448 sublisting = json.loads(resp.body)
449 if not sublisting:
450 break
451 for container in sublisting:
452 if container['name'][0] != '.':
453 listing.append({'name': container['name']})
454 marker = sublisting[-1]['name']
455 return Response(body=json.dumps({'accounts': listing}))
456
457 def handle_get_account(self, req):
458 """
459 Handles the GET v2/<account> call for getting account information.
460 Can only be called by an account .admin.
461
462 On success, a JSON dictionary will be returned containing the keys
463 `account_id`, `services`, and `users`. The `account_id` is the value
464 used when creating service accounts. The `services` value is a dict as
465 described in the :func:`handle_get_token` call. The `users` value is a
466 list of dicts, each dict representing a user and currently only
467 containing the single key `name`. For example::
468
469 {"account_id": "AUTH_018c3946-23f8-4efb-a8fb-b67aae8e4162",
470 "services": {"storage": {"default": "local",
471 "local": "http://127.0.0.1:8080/v1/AUTH_018c3946"}},
472 "users": [{"name": "tester"}, {"name": "tester3"}]}
473
474 :param req: The webob.Request to process.
475 :returns: webob.Response, 2xx on success with a JSON dictionary as
476 explained above.
477 """
478 account = req.path_info_pop()
479 if req.path_info or not account or account[0] == '.':
480 return HTTPBadRequest(request=req)
481 if not self.is_account_admin(req, account):
482 return HTTPForbidden(request=req)
483 path = quote('/v1/%s/%s/.services' % (self.auth_account, account))
484 resp = self.make_request(req.environ, 'GET',
485 path).get_response(self.app)
486 if resp.status_int == 404:
487 return HTTPNotFound(request=req)
488 if resp.status_int // 100 != 2:
489 raise Exception('Could not obtain the .services object: %s %s' %
490 (path, resp.status))
491 services = json.loads(resp.body)
492 listing = []
493 marker = ''
494 while True:
495 path = '/v1/%s?format=json&marker=%s' % (quote('%s/%s' %
496 (self.auth_account, account)), quote(marker))
497 resp = self.make_request(req.environ, 'GET',
498 path).get_response(self.app)
499 if resp.status_int == 404:
500 return HTTPNotFound(request=req)
501 if resp.status_int // 100 != 2:
502 raise Exception('Could not list in main auth account: %s %s' %
503 (path, resp.status))
504 account_id = resp.headers['X-Container-Meta-Account-Id']
505 sublisting = json.loads(resp.body)
506 if not sublisting:
507 break
508 for obj in sublisting:
509 if obj['name'][0] != '.':
510 listing.append({'name': obj['name']})
511 marker = sublisting[-1]['name']
512 return Response(body=json.dumps({'account_id': account_id,
513 'services': services, 'users': listing}))
514
515 def handle_set_services(self, req):
516 """
517 Handles the POST v2/<account>/.services call for setting services
518 information. Can only be called by a reseller .admin.
519
520 In the :func:`handle_get_account` (GET v2/<account>) call, a section of
521 the returned JSON dict is `services`. This section looks something like
522 this::
523
524 "services": {"storage": {"default": "local",
525 "local": "http://127.0.0.1:8080/v1/AUTH_018c3946"}}
526
527 Making use of this section is described in :func:`handle_get_token`.
528
529 This function allows setting values within this section for the
530 <account>, allowing the addition of new service end points or updating
531 existing ones.
532
533 The body of the POST request should contain a JSON dict with the
534 following format::
535
536 {"service_name": {"end_point_name": "end_point_value"}}
537
538 There can be multiple services and multiple end points in the same
539 call.
540
541 Any new services or end points will be added to the existing set of
542 services and end points. Any existing services with the same service
543 name will be merged with the new end points. Any existing end points
544 with the same end point name will have their values updated.
545
546 The updated services dictionary will be returned on success.
547
548 :param req: The webob.Request to process.
549 :returns: webob.Response, 2xx on success with the udpated services JSON
550 dict as described above
551 """
552 if not self.is_reseller_admin(req):
553 return HTTPForbidden(request=req)
554 account = req.path_info_pop()
555 if req.path_info != '/.services' or not account or account[0] == '.':
556 return HTTPBadRequest(request=req)
557 try:
558 new_services = json.loads(req.body)
559 except ValueError, err:
560 return HTTPBadRequest(body=str(err))
561 # Get the current services information
562 path = quote('/v1/%s/%s/.services' % (self.auth_account, account))
563 resp = self.make_request(req.environ, 'GET',
564 path).get_response(self.app)
565 if resp.status_int == 404:
566 return HTTPNotFound(request=req)
567 if resp.status_int // 100 != 2:
568 raise Exception('Could not obtain services info: %s %s' %
569 (path, resp.status))
570 services = json.loads(resp.body)
571 for new_service, value in new_services.iteritems():
572 if new_service in services:
573 services[new_service].update(value)
574 else:
575 services[new_service] = value
576 # Save the new services information
577 services = json.dumps(services)
578 resp = self.make_request(req.environ, 'PUT', path,
579 services).get_response(self.app)
580 if resp.status_int // 100 != 2:
581 raise Exception('Could not save .services object: %s %s' %
582 (path, resp.status))
583 return Response(request=req, body=services)
584
585 def handle_put_account(self, req):
586 """
587 Handles the PUT v2/<account> call for adding an account to the auth
588 system. Can only be called by a .reseller_admin.
589
590 By default, a newly created UUID4 will be used with the reseller prefix
591 as the account id used when creating corresponding service accounts.
592 However, you can provide an X-Account-Suffix header to replace the
593 UUID4 part.
594
595 :param req: The webob.Request to process.
596 :returns: webob.Response, 2xx on success.
597 """
598 if not self.is_reseller_admin(req):
599 return HTTPForbidden(request=req)
600 account = req.path_info_pop()
601 if req.path_info or not account or account[0] == '.':
602 return HTTPBadRequest(request=req)
603 # Ensure the container in the main auth account exists (this
604 # container represents the new account)
605 path = quote('/v1/%s/%s' % (self.auth_account, account))
606 resp = self.make_request(req.environ, 'HEAD',
607 path).get_response(self.app)
608 if resp.status_int == 404:
609 resp = self.make_request(req.environ, 'PUT',
610 path).get_response(self.app)
611 if resp.status_int // 100 != 2:
612 raise Exception('Could not create account within main auth '
613 'account: %s %s' % (path, resp.status))
614 elif resp.status_int // 100 == 2:
615 if 'x-container-meta-account-id' in resp.headers:
616 # Account was already created
617 return HTTPAccepted(request=req)
618 else:
619 raise Exception('Could not verify account within main auth '
620 'account: %s %s' % (path, resp.status))
621 account_suffix = req.headers.get('x-account-suffix')
622 if not account_suffix:
623 account_suffix = str(uuid4())
624 # Create the new account in the Swift cluster
625 path = quote('%s/%s%s' % (self.dsc_parsed2.path,
626 self.reseller_prefix, account_suffix))
627 try:
628 conn = self.get_conn()
629 conn.request('PUT', path,
630 headers={'X-Auth-Token': self.get_itoken(req.environ)})
631 resp = conn.getresponse()
632 resp.read()
633 if resp.status // 100 != 2:
634 raise Exception('Could not create account on the Swift '
635 'cluster: %s %s %s' % (path, resp.status, resp.reason))
636 except (Exception, TimeoutError):
637 self.logger.error(_('ERROR: Exception while trying to communicate '
638 'with %(scheme)s://%(host)s:%(port)s/%(path)s'),
639 {'scheme': self.dsc_parsed2.scheme,
640 'host': self.dsc_parsed2.hostname,
641 'port': self.dsc_parsed2.port, 'path': path})
642 raise
643 # Record the mapping from account id back to account name
644 path = quote('/v1/%s/.account_id/%s%s' %
645 (self.auth_account, self.reseller_prefix, account_suffix))
646 resp = self.make_request(req.environ, 'PUT', path,
647 account).get_response(self.app)
648 if resp.status_int // 100 != 2:
649 raise Exception('Could not create account id mapping: %s %s' %
650 (path, resp.status))
651 # Record the cluster url(s) for the account
652 path = quote('/v1/%s/%s/.services' % (self.auth_account, account))
653 services = {'storage': {}}
654 services['storage'][self.dsc_name] = '%s/%s%s' % (self.dsc_url,
655 self.reseller_prefix, account_suffix)
656 services['storage']['default'] = self.dsc_name
657 resp = self.make_request(req.environ, 'PUT', path,
658 json.dumps(services)).get_response(self.app)
659 if resp.status_int // 100 != 2:
660 raise Exception('Could not create .services object: %s %s' %
661 (path, resp.status))
662 # Record the mapping from account name to the account id
663 path = quote('/v1/%s/%s' % (self.auth_account, account))
664 resp = self.make_request(req.environ, 'POST', path,
665 headers={'X-Container-Meta-Account-Id': '%s%s' %
666 (self.reseller_prefix, account_suffix)}).get_response(self.app)
667 if resp.status_int // 100 != 2:
668 raise Exception('Could not record the account id on the account: '
669 '%s %s' % (path, resp.status))
670 return HTTPCreated(request=req)
671
672 def handle_delete_account(self, req):
673 """
674 Handles the DELETE v2/<account> call for removing an account from the
675 auth system. Can only be called by a .reseller_admin.
676
677 :param req: The webob.Request to process.
678 :returns: webob.Response, 2xx on success.
679 """
680 if not self.is_reseller_admin(req):
681 return HTTPForbidden(request=req)
682 account = req.path_info_pop()
683 if req.path_info or not account or account[0] == '.':
684 return HTTPBadRequest(request=req)
685 # Make sure the account has no users and get the account_id
686 marker = ''
687 while True:
688 path = '/v1/%s?format=json&marker=%s' % (quote('%s/%s' %
689 (self.auth_account, account)), quote(marker))
690 resp = self.make_request(req.environ, 'GET',
691 path).get_response(self.app)
692 if resp.status_int == 404:
693 return HTTPNotFound(request=req)
694 if resp.status_int // 100 != 2:
695 raise Exception('Could not list in main auth account: %s %s' %
696 (path, resp.status))
697 account_id = resp.headers['x-container-meta-account-id']
698 sublisting = json.loads(resp.body)
699 if not sublisting:
700 break
701 for obj in sublisting:
702 if obj['name'][0] != '.':
703 return HTTPConflict(request=req)
704 marker = sublisting[-1]['name']
705 # Obtain the listing of services the account is on.
706 path = quote('/v1/%s/%s/.services' % (self.auth_account, account))
707 resp = self.make_request(req.environ, 'GET',
708 path).get_response(self.app)
709 if resp.status_int // 100 != 2 and resp.status_int != 404:
710 raise Exception('Could not obtain .services object: %s %s' %
711 (path, resp.status))
712 if resp.status_int // 100 == 2:
713 services = json.loads(resp.body)
714 # Delete the account on each cluster it is on.
715 deleted_any = False
716 for name, url in services['storage'].iteritems():
717 if name != 'default':
718 parsed = urlparse(url)
719 conn = self.get_conn(parsed)
720 conn.request('DELETE', parsed.path,
721 headers={'X-Auth-Token': self.get_itoken(req.environ)})
722 resp = conn.getresponse()
723 resp.read()
724 if resp.status == 409:
725 if deleted_any:
726 raise Exception('Managed to delete one or more '
727 'service end points, but failed with: '
728 '%s %s %s' % (url, resp.status, resp.reason))
729 else:
730 return HTTPConflict(request=req)
731 if resp.status // 100 != 2 and resp.status != 404:
732 raise Exception('Could not delete account on the '
733 'Swift cluster: %s %s %s' %
734 (url, resp.status, resp.reason))
735 deleted_any = True
736 # Delete the .services object itself.
737 path = quote('/v1/%s/%s/.services' %
738 (self.auth_account, account))
739 resp = self.make_request(req.environ, 'DELETE',
740 path).get_response(self.app)
741 if resp.status_int // 100 != 2 and resp.status_int != 404:
742 raise Exception('Could not delete .services object: %s %s' %
743 (path, resp.status))
744 # Delete the account id mapping for the account.
745 path = quote('/v1/%s/.account_id/%s' %
746 (self.auth_account, account_id))
747 resp = self.make_request(req.environ, 'DELETE',
748 path).get_response(self.app)
749 if resp.status_int // 100 != 2 and resp.status_int != 404:
750 raise Exception('Could not delete account id mapping: %s %s' %
751 (path, resp.status))
752 # Delete the account marker itself.
753 path = quote('/v1/%s/%s' % (self.auth_account, account))
754 resp = self.make_request(req.environ, 'DELETE',
755 path).get_response(self.app)
756 if resp.status_int // 100 != 2 and resp.status_int != 404:
757 raise Exception('Could not delete account marked: %s %s' %
758 (path, resp.status))
759 return HTTPNoContent(request=req)
760
761 def handle_get_user(self, req):
762 """
763 Handles the GET v2/<account>/<user> call for getting user information.
764 Can only be called by an account .admin.
765
766 On success, a JSON dict will be returned as described::
767
768 {"groups": [ # List of groups the user is a member of
769 {"name": "<act>:<usr>"},
770 # The first group is a unique user identifier
771 {"name": "<account>"},
772 # The second group is the auth account name
773 {"name": "<additional-group>"}
774 # There may be additional groups, .admin being a special
775 # group indicating an account admin and .reseller_admin
776 # indicating a reseller admin.
777 ],
778 "auth": "plaintext:<key>"
779 # The auth-type and key for the user; currently only plaintext is
780 # implemented.
781 }
782
783 For example::
784
785 {"groups": [{"name": "test:tester"}, {"name": "test"},
786 {"name": ".admin"}],
787 "auth": "plaintext:testing"}
788
789 If the <user> in the request is the special user `.groups`, the JSON
790 dict will contain a single key of `groups` whose value is a list of
791 dicts representing the active groups within the account. Each dict
792 currently has the single key `name`. For example::
793
794 {"groups": [{"name": ".admin"}, {"name": "test"},
795 {"name": "test:tester"}, {"name": "test:tester3"}]}
796
797 :param req: The webob.Request to process.
798 :returns: webob.Response, 2xx on success with a JSON dictionary as
799 explained above.
800 """
801 account = req.path_info_pop()
802 user = req.path_info_pop()
803 if req.path_info or not account or account[0] == '.' or not user or \
804 (user[0] == '.' and user != '.groups'):
805 return HTTPBadRequest(request=req)
806 if not self.is_account_admin(req, account):
807 return HTTPForbidden(request=req)
808 if user == '.groups':
809 # TODO: This could be very slow for accounts with a really large
810 # number of users. Speed could be improved by concurrently
811 # requesting user group information. Then again, I don't *know*
812 # it's slow for `normal` use cases, so testing should be done.
813 groups = set()
814 marker = ''
815 while True:
816 path = '/v1/%s?format=json&marker=%s' % (quote('%s/%s' %
817 (self.auth_account, account)), quote(marker))
818 resp = self.make_request(req.environ, 'GET',
819 path).get_response(self.app)
820 if resp.status_int == 404:
821 return HTTPNotFound(request=req)
822 if resp.status_int // 100 != 2:
823 raise Exception('Could not list in main auth account: '
824 '%s %s' % (path, resp.status))
825 sublisting = json.loads(resp.body)
826 if not sublisting:
827 break
828 for obj in sublisting:
829 if obj['name'][0] != '.':
830 path = quote('/v1/%s/%s/%s' % (self.auth_account,
831 account, obj['name']))
832 resp = self.make_request(req.environ, 'GET',
833 path).get_response(self.app)
834 if resp.status_int // 100 != 2:
835 raise Exception('Could not retrieve user object: '
836 '%s %s' % (path, resp.status))
837 groups.update(g['name']
838 for g in json.loads(resp.body)['groups'])
839 marker = sublisting[-1]['name']
840 body = json.dumps({'groups':
841 [{'name': g} for g in sorted(groups)]})
842 else:
843 path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user))
844 resp = self.make_request(req.environ, 'GET',
845 path).get_response(self.app)
846 if resp.status_int == 404:
847 return HTTPNotFound(request=req)
848 if resp.status_int // 100 != 2:
849 raise Exception('Could not retrieve user object: %s %s' %
850 (path, resp.status))
851 body = resp.body
852 display_groups = [g['name'] for g in json.loads(body)['groups']]
853 if ('.admin' in display_groups and
854 not self.is_reseller_admin(req)) or \
855 ('.reseller_admin' in display_groups and
856 not self.is_super_admin(req)):
857 return HTTPForbidden(request=req)
858 return Response(body=body)
859
860 def handle_put_user(self, req):
861 """
862 Handles the PUT v2/<account>/<user> call for adding a user to an
863 account.
864
865 X-Auth-User-Key represents the user's key, X-Auth-User-Admin may be set
866 to `true` to create an account .admin, and X-Auth-User-Reseller-Admin
867 may be set to `true` to create a .reseller_admin.
868
869 Can only be called by an account .admin unless the user is to be a
870 .reseller_admin, in which case the request must be by .super_admin.
871
872 :param req: The webob.Request to process.
873 :returns: webob.Response, 2xx on success.
874 """
875 # Validate path info
876 account = req.path_info_pop()
877 user = req.path_info_pop()
878 key = req.headers.get('x-auth-user-key')
879 admin = req.headers.get('x-auth-user-admin') == 'true'
880 reseller_admin = \
881 req.headers.get('x-auth-user-reseller-admin') == 'true'
882 if reseller_admin:
883 admin = True
884 if req.path_info or not account or account[0] == '.' or not user or \
885 user[0] == '.' or not key:
886 return HTTPBadRequest(request=req)
887 if reseller_admin:
888 if not self.is_super_admin(req):
889 return HTTPForbidden(request=req)
890 elif not self.is_account_admin(req, account):
891 return HTTPForbidden(request=req)
892
893 path = quote('/v1/%s/%s' % (self.auth_account, account))
894 resp = self.make_request(req.environ, 'HEAD',
895 path).get_response(self.app)
896 if resp.status_int // 100 != 2:
897 raise Exception('Could not retrieve account id value: %s %s' %
898 (path, resp.status))
899 headers = {'X-Object-Meta-Account-Id':
900 resp.headers['x-container-meta-account-id']}
901 # Create the object in the main auth account (this object represents
902 # the user)
903 path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user))
904 groups = ['%s:%s' % (account, user), account]
905 if admin:
906 groups.append('.admin')
907 if reseller_admin:
908 groups.append('.reseller_admin')
909 resp = self.make_request(req.environ, 'PUT', path,
910 json.dumps({'auth': 'plaintext:%s' % key,
911 'groups': [{'name': g} for g in groups]}),
912 headers=headers).get_response(self.app)
913 if resp.status_int == 404:
914 return HTTPNotFound(request=req)
915 if resp.status_int // 100 != 2:
916 raise Exception('Could not create user object: %s %s' %
917 (path, resp.status))
918 return HTTPCreated(request=req)
919
920 def handle_delete_user(self, req):
921 """
922 Handles the DELETE v2/<account>/<user> call for deleting a user from an
923 account.
924
925 Can only be called by an account .admin.
926
927 :param req: The webob.Request to process.
928 :returns: webob.Response, 2xx on success.
929 """
930 # Validate path info
931 account = req.path_info_pop()
932 user = req.path_info_pop()
933 if req.path_info or not account or account[0] == '.' or not user or \
934 user[0] == '.':
935 return HTTPBadRequest(request=req)
936 if not self.is_account_admin(req, account):
937 return HTTPForbidden(request=req)
938 # Delete the user's existing token, if any.
939 path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user))
940 resp = self.make_request(req.environ, 'HEAD',
941 path).get_response(self.app)
942 if resp.status_int == 404:
943 return HTTPNotFound(request=req)
944 elif resp.status_int // 100 != 2:
945 raise Exception('Could not obtain user details: %s %s' %
946 (path, resp.status))
947 candidate_token = resp.headers.get('x-object-meta-auth-token')
948 if candidate_token:
949 path = quote('/v1/%s/.token_%s/%s' %
950 (self.auth_account, candidate_token[-1], candidate_token))
951 resp = self.make_request(req.environ, 'DELETE',
952 path).get_response(self.app)
953 if resp.status_int // 100 != 2 and resp.status_int != 404:
954 raise Exception('Could not delete possibly existing token: '
955 '%s %s' % (path, resp.status))
956 # Delete the user entry itself.
957 path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user))
958 resp = self.make_request(req.environ, 'DELETE',
959 path).get_response(self.app)
960 if resp.status_int // 100 != 2 and resp.status_int != 404:
961 raise Exception('Could not delete the user object: %s %s' %
962 (path, resp.status))
963 return HTTPNoContent(request=req)
964
965 def handle_get_token(self, req):
966 """
967 Handles the various `request for token and service end point(s)` calls.
968 There are various formats to support the various auth servers in the
969 past. Examples::
970
971 GET <auth-prefix>/v1/<act>/auth
972 X-Auth-User: <act>:<usr> or X-Storage-User: <usr>
973 X-Auth-Key: <key> or X-Storage-Pass: <key>
974 GET <auth-prefix>/auth
975 X-Auth-User: <act>:<usr> or X-Storage-User: <act>:<usr>
976 X-Auth-Key: <key> or X-Storage-Pass: <key>
977 GET <auth-prefix>/v1.0
978 X-Auth-User: <act>:<usr> or X-Storage-User: <act>:<usr>
979 X-Auth-Key: <key> or X-Storage-Pass: <key>
980
981 On successful authentication, the response will have X-Auth-Token and
982 X-Storage-Token set to the token to use with Swift and X-Storage-URL
983 set to the URL to the default Swift cluster to use.
984
985 The response body will be set to the account's services JSON object as
986 described here::
987
988 {"storage": { # Represents the Swift storage service end points
989 "default": "cluster1", # Indicates which cluster is the default
990 "cluster1": "<URL to use with Swift>",
991 # A Swift cluster that can be used with this account,
992 # "cluster1" is the name of the cluster which is usually a
993 # location indicator (like "dfw" for a datacenter region).
994 "cluster2": "<URL to use with Swift>"
995 # Another Swift cluster that can be used with this account,
996 # there will always be at least one Swift cluster to use or
997 # this whole "storage" dict won't be included at all.
998 },
999 "servers": { # Represents the Nova server service end points
1000 # Expected to be similar to the "storage" dict, but not
1001 # implemented yet.
1002 },
1003 # Possibly other service dicts, not implemented yet.
1004 }
1005
1006 :param req: The webob.Request to process.
1007 :returns: webob.Response, 2xx on success with data set as explained
1008 above.
1009 """
1010 # Validate the request info
1011 try:
1012 pathsegs = split_path(req.path_info, minsegs=1, maxsegs=3,
1013 rest_with_last=True)
1014 except ValueError:
1015 return HTTPNotFound(request=req)
1016 if pathsegs[0] == 'v1' and pathsegs[2] == 'auth':
1017 account = pathsegs[1]
1018 user = req.headers.get('x-storage-user')
1019 if not user:
1020 user = req.headers.get('x-auth-user')
1021 if not user or ':' not in user:
1022 return HTTPUnauthorized(request=req)
1023 account2, user = user.split(':', 1)
1024 if account != account2:
1025 return HTTPUnauthorized(request=req)
1026 key = req.headers.get('x-storage-pass')
1027 if not key:
1028 key = req.headers.get('x-auth-key')
1029 elif pathsegs[0] in ('auth', 'v1.0'):
1030 user = req.headers.get('x-auth-user')
1031 if not user:
1032 user = req.headers.get('x-storage-user')
1033 if not user or ':' not in user:
1034 return HTTPUnauthorized(request=req)
1035 account, user = user.split(':', 1)
1036 key = req.headers.get('x-auth-key')
1037 if not key:
1038 key = req.headers.get('x-storage-pass')
1039 else:
1040 return HTTPBadRequest(request=req)
1041 if not all((account, user, key)):
1042 return HTTPUnauthorized(request=req)
1043 if user == '.super_admin' and key == self.super_admin_key:
1044 token = self.get_itoken(req.environ)
1045 url = '%s/%s.auth' % (self.dsc_url, self.reseller_prefix)
1046 return Response(request=req,
1047 body=json.dumps({'storage': {'default': 'local', 'local': url}}),
1048 headers={'x-auth-token': token, 'x-storage-token': token,
1049 'x-storage-url': url})
1050 # Authenticate user
1051 path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user))
1052 resp = self.make_request(req.environ, 'GET',
1053 path).get_response(self.app)
1054 if resp.status_int == 404:
1055 return HTTPUnauthorized(request=req)
1056 if resp.status_int // 100 != 2:
1057 raise Exception('Could not obtain user details: %s %s' %
1058 (path, resp.status))
1059 user_detail = json.loads(resp.body)
1060 if not self.credentials_match(user_detail, key):
1061 return HTTPUnauthorized(request=req)
1062 # See if a token already exists and hasn't expired
1063 token = None
1064 candidate_token = resp.headers.get('x-object-meta-auth-token')
1065 if candidate_token:
1066 path = quote('/v1/%s/.token_%s/%s' %
1067 (self.auth_account, candidate_token[-1], candidate_token))
1068 resp = self.make_request(req.environ, 'GET',
1069 path).get_response(self.app)
1070 if resp.status_int // 100 == 2:
1071 token_detail = json.loads(resp.body)
1072 if token_detail['expires'] > time():
1073 token = candidate_token
1074 else:
1075 self.make_request(req.environ, 'DELETE',
1076 path).get_response(self.app)
1077 elif resp.status_int != 404:
1078 raise Exception('Could not detect whether a token already '
1079 'exists: %s %s' % (path, resp.status))
1080 # Create a new token if one didn't exist
1081 if not token:
1082 # Retrieve account id, we'll save this in the token
1083 path = quote('/v1/%s/%s' % (self.auth_account, account))
1084 resp = self.make_request(req.environ, 'HEAD',
1085 path).get_response(self.app)
1086 if resp.status_int // 100 != 2:
1087 raise Exception('Could not retrieve account id value: '
1088 '%s %s' % (path, resp.status))
1089 account_id = \
1090 resp.headers['x-container-meta-account-id']
1091 # Generate new token
1092 token = '%stk%s' % (self.reseller_prefix, uuid4().hex)
1093 # Save token info
1094 path = quote('/v1/%s/.token_%s/%s' %
1095 (self.auth_account, token[-1], token))
1096 resp = self.make_request(req.environ, 'PUT', path,
1097 json.dumps({'account': account, 'user': user,
1098 'account_id': account_id,
1099 'groups': user_detail['groups'],
1100 'expires': time() + self.token_life})).get_response(self.app)
1101 if resp.status_int // 100 != 2:
1102 raise Exception('Could not create new token: %s %s' %
1103 (path, resp.status))
1104 # Record the token with the user info for future use.
1105 path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user))
1106 resp = self.make_request(req.environ, 'POST', path,
1107 headers={'X-Object-Meta-Auth-Token': token}
1108 ).get_response(self.app)
1109 if resp.status_int // 100 != 2:
1110 raise Exception('Could not save new token: %s %s' %
1111 (path, resp.status))
1112 # Get the services information
1113 path = quote('/v1/%s/%s/.services' % (self.auth_account, account))
1114 resp = self.make_request(req.environ, 'GET',
1115 path).get_response(self.app)
1116 if resp.status_int // 100 != 2:
1117 raise Exception('Could not obtain services info: %s %s' %
1118 (path, resp.status))
1119 detail = json.loads(resp.body)
1120 url = detail['storage'][detail['storage']['default']]
1121 return Response(request=req, body=resp.body,
1122 headers={'x-auth-token': token, 'x-storage-token': token,
1123 'x-storage-url': url})
1124
1125 def handle_validate_token(self, req):
1126 """
1127 Handles the GET v2/.token/<token> call for validating a token, usually
1128 called by a service like Swift.
1129
1130 On a successful validation, X-Auth-TTL will be set for how much longer
1131 this token is valid and X-Auth-Groups will contain a comma separated
1132 list of groups the user belongs to.
1133
1134 The first group listed will be a unique identifier for the user the
1135 token represents.
1136
1137 .reseller_admin is a special group that indicates the user should be
1138 allowed to do anything on any account.
1139
1140 :param req: The webob.Request to process.
1141 :returns: webob.Response, 2xx on success with data set as explained
1142 above.
1143 """
1144 token = req.path_info_pop()
1145 if req.path_info or not token.startswith(self.reseller_prefix):
1146 return HTTPBadRequest(request=req)
1147 expires = groups = None
1148 memcache_client = cache_from_env(req.environ)
1149 if memcache_client:
1150 memcache_key = '%s/auth/%s' % (self.reseller_prefix, token)
1151 cached_auth_data = memcache_client.get(memcache_key)
1152 if cached_auth_data:
1153 expires, groups = cached_auth_data
1154 if expires < time():
1155 groups = None
1156 if not groups:
1157 path = quote('/v1/%s/.token_%s/%s' %
1158 (self.auth_account, token[-1], token))
1159 resp = self.make_request(req.environ, 'GET',
1160 path).get_response(self.app)
1161 if resp.status_int // 100 != 2:
1162 return HTTPNotFound(request=req)
1163 detail = json.loads(resp.body)
1164 expires = detail['expires']
1165 if expires < time():
1166 self.make_request(req.environ, 'DELETE',
1167 path).get_response(self.app)
1168 return HTTPNotFound(request=req)
1169 groups = [g['name'] for g in detail['groups']]
1170 if '.admin' in groups:
1171 groups.remove('.admin')
1172 groups.append(detail['account_id'])
1173 groups = ','.join(groups)
1174 return HTTPNoContent(headers={'X-Auth-TTL': expires - time(),
1175 'X-Auth-Groups': groups})
1176
1177 def make_request(self, env, method, path, body=None, headers=None):
1178 """
1179 Makes a new webob.Request based on the current env but with the
1180 parameters specified.
1181
1182 :param env: Current WSGI environment dictionary
1183 :param method: HTTP method of new request
1184 :param path: HTTP path of new request
1185 :param body: HTTP body of new request; None by default
1186 :param headers: Extra HTTP headers of new request; None by default
1187
1188 :returns: webob.Request object
1189 """
1190 newenv = {'REQUEST_METHOD': method, 'HTTP_USER_AGENT': 'Swauth'}
1191 for name in ('swift.cache', 'HTTP_X_CF_TRANS_ID'):
1192 if name in env:
1193 newenv[name] = env[name]
1194 if not headers:
1195 headers = {}
1196 if body:
1197 return Request.blank(path, environ=newenv, body=body,
1198 headers=headers)
1199 else:
1200 return Request.blank(path, environ=newenv, headers=headers)
1201
1202 def get_conn(self, urlparsed=None):
1203 """
1204 Returns an HTTPConnection based on the urlparse result given or the
1205 default Swift cluster (internal url) urlparse result.
1206
1207 :param urlparsed: The result from urlparse.urlparse or None to use the
1208 default Swift cluster's value
1209 """
1210 if not urlparsed:
1211 urlparsed = self.dsc_parsed2
1212 if urlparsed.scheme == 'http':
1213 return HTTPConnection(urlparsed.netloc)
1214 else:
1215 return HTTPSConnection(urlparsed.netloc)
1216
1217 def get_itoken(self, env):
1218 """
1219 Returns the current internal token to use for the auth system's own
1220 actions with other services. Each process will create its own
1221 itoken and the token will be deleted and recreated based on the
1222 token_life configuration value. The itoken information is stored in
1223 memcache because the auth process that is asked by Swift to validate
1224 the token may not be the same as the auth process that created the
1225 token.
1226 """
1227 if not self.itoken or self.itoken_expires < time():
1228 self.itoken = '%sitk%s' % (self.reseller_prefix, uuid4().hex)
1229 memcache_key = '%s/auth/%s' % (self.reseller_prefix, self.itoken)
1230 self.itoken_expires = time() + self.token_life - 60
1231 memcache_client = cache_from_env(env)
1232 if not memcache_client:
1233 raise Exception(
1234 'No memcache set up; required for Swauth middleware')
1235 memcache_client.set(memcache_key, (self.itoken_expires,
1236 '.auth,.reseller_admin,%s.auth' % self.reseller_prefix),
1237 timeout=self.token_life)
1238 return self.itoken
1239
1240 def get_admin_detail(self, req):
1241 """
1242 Returns the dict for the user specified as the admin in the request
1243 with the addition of an `account` key set to the admin user's account.
1244
1245 :param req: The webob request to retrieve X-Auth-Admin-User and
1246 X-Auth-Admin-Key from.
1247 :returns: The dict for the admin user with the addition of the
1248 `account` key.
1249 """
1250 if ':' not in req.headers.get('x-auth-admin-user', ''):
1251 return None
1252 admin_account, admin_user = \
1253 req.headers.get('x-auth-admin-user').split(':', 1)
1254 path = quote('/v1/%s/%s/%s' % (self.auth_account, admin_account,
1255 admin_user))
1256 resp = self.make_request(req.environ, 'GET',
1257 path).get_response(self.app)
1258 if resp.status_int == 404:
1259 return None
1260 if resp.status_int // 100 != 2:
1261 raise Exception('Could not get admin user object: %s %s' %
1262 (path, resp.status))
1263 admin_detail = json.loads(resp.body)
1264 admin_detail['account'] = admin_account
1265 return admin_detail
1266
1267 def credentials_match(self, user_detail, key):
1268 """
1269 Returns True if the key is valid for the user_detail. Currently, this
1270 only supports plaintext key matching.
1271
1272 :param user_detail: The dict for the user.
1273 :param key: The key to validate for the user.
1274 :returns: True if the key is valid for the user, False if not.
1275 """
1276 return user_detail and user_detail.get('auth') == 'plaintext:%s' % key
1277
1278 def is_super_admin(self, req):
1279 """
1280 Returns True if the admin specified in the request represents the
1281 .super_admin.
1282
1283 :param req: The webob.Request to check.
1284 :param returns: True if .super_admin.
1285 """
1286 return req.headers.get('x-auth-admin-user') == '.super_admin' and \
1287 req.headers.get('x-auth-admin-key') == self.super_admin_key
1288
1289 def is_reseller_admin(self, req, admin_detail=None):
1290 """
1291 Returns True if the admin specified in the request represents a
1292 .reseller_admin.
1293
1294 :param req: The webob.Request to check.
1295 :param admin_detail: The previously retrieved dict from
1296 :func:`get_admin_detail` or None for this function
1297 to retrieve the admin_detail itself.
1298 :param returns: True if .reseller_admin.
1299 """
1300 if self.is_super_admin(req):
1301 return True
1302 if not admin_detail:
1303 admin_detail = self.get_admin_detail(req)
1304 if not self.credentials_match(admin_detail,
1305 req.headers.get('x-auth-admin-key')):
1306 return False
1307 return '.reseller_admin' in (g['name'] for g in admin_detail['groups'])
1308
1309 def is_account_admin(self, req, account):
1310 """
1311 Returns True if the admin specified in the request represents a .admin
1312 for the account specified.
1313
1314 :param req: The webob.Request to check.
1315 :param account: The account to check for .admin against.
1316 :param returns: True if .admin.
1317 """
1318 if self.is_super_admin(req):
1319 return True
1320 admin_detail = self.get_admin_detail(req)
1321 if admin_detail:
1322 if self.is_reseller_admin(req, admin_detail=admin_detail):
1323 return True
1324 if not self.credentials_match(admin_detail,
1325 req.headers.get('x-auth-admin-key')):
1326 return False
1327 return admin_detail and admin_detail['account'] == account and \
1328 '.admin' in (g['name'] for g in admin_detail['groups'])
1329 return False
1330
1331 def posthooklogger(self, env, req):
1332 if not req.path.startswith(self.auth_prefix):
1333 return
1334 response = getattr(req, 'response', None)
1335 if not response:
1336 return
1337 trans_time = '%.4f' % (time() - req.start_time)
1338 the_request = quote(unquote(req.path))
1339 if req.query_string:
1340 the_request = the_request + '?' + req.query_string
1341 # remote user for zeus
1342 client = req.headers.get('x-cluster-client-ip')
1343 if not client and 'x-forwarded-for' in req.headers:
1344 # remote user for other lbs
1345 client = req.headers['x-forwarded-for'].split(',')[0].strip()
1346 logged_headers = None
1347 if self.log_headers:
1348 logged_headers = '\n'.join('%s: %s' % (k, v)
1349 for k, v in req.headers.items())
1350 status_int = response.status_int
1351 if getattr(req, 'client_disconnect', False) or \
1352 getattr(response, 'client_disconnect', False):
1353 status_int = 499
1354 self.logger.info(' '.join(quote(str(x)) for x in (client or '-',
1355 req.remote_addr or '-', strftime('%d/%b/%Y/%H/%M/%S', gmtime()),
1356 req.method, the_request, req.environ['SERVER_PROTOCOL'],
1357 status_int, req.referer or '-', req.user_agent or '-',
1358 req.headers.get('x-auth-token',
1359 req.headers.get('x-auth-admin-user', '-')),
1360 getattr(req, 'bytes_transferred', 0) or '-',
1361 getattr(response, 'bytes_transferred', 0) or '-',
1362 req.headers.get('etag', '-'),
1363 req.headers.get('x-trans-id', '-'), logged_headers or '-',
1364 trans_time)))
1365
1366
1367def filter_factory(global_conf, **local_conf):
1368 """Returns a WSGI filter app for use with paste.deploy."""
1369 conf = global_conf.copy()
1370 conf.update(local_conf)
1371
1372 def auth_filter(app):
1373 return Swauth(app, conf)
1374 return auth_filter
13750
=== added file 'swift/common/middleware/tempauth.py'
--- swift/common/middleware/tempauth.py 1970-01-01 00:00:00 +0000
+++ swift/common/middleware/tempauth.py 2011-06-09 21:09:39 +0000
@@ -0,0 +1,482 @@
1# Copyright (c) 2011 OpenStack, LLC.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12# implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16from time import gmtime, strftime, time
17from traceback import format_exc
18from urllib import quote, unquote
19from uuid import uuid4
20from hashlib import sha1
21import hmac
22import base64
23
24from eventlet import TimeoutError
25from webob import Response, Request
26from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPNotFound, \
27 HTTPUnauthorized
28
29from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
30from swift.common.utils import cache_from_env, get_logger, split_path
31
32
33class TempAuth(object):
34 """
35 Test authentication and authorization system.
36
37 Add to your pipeline in proxy-server.conf, such as::
38
39 [pipeline:main]
40 pipeline = catch_errors cache tempauth proxy-server
41
42 And add a tempauth filter section, such as::
43
44 [filter:tempauth]
45 use = egg:swift#tempauth
46 user_admin_admin = admin .admin .reseller_admin
47 user_test_tester = testing .admin
48 user_test2_tester2 = testing2 .admin
49 user_test_tester3 = testing3
50
51 See the proxy-server.conf-sample for more information.
52
53 :param app: The next WSGI app in the pipeline
54 :param conf: The dict of configuration values
55 """
56
57 def __init__(self, app, conf):
58 self.app = app
59 self.conf = conf
60 self.logger = get_logger(conf, log_route='tempauth')
61 self.log_headers = conf.get('log_headers') == 'True'
62 self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip()
63 if self.reseller_prefix and self.reseller_prefix[-1] != '_':
64 self.reseller_prefix += '_'
65 self.auth_prefix = conf.get('auth_prefix', '/auth/')
66 if not self.auth_prefix:
67 self.auth_prefix = '/auth/'
68 if self.auth_prefix[0] != '/':
69 self.auth_prefix = '/' + self.auth_prefix
70 if self.auth_prefix[-1] != '/':
71 self.auth_prefix += '/'
72 self.token_life = int(conf.get('token_life', 86400))
73 self.users = {}
74 for conf_key in conf:
75 if conf_key.startswith('user_'):
76 values = conf[conf_key].split()
77 if not values:
78 raise ValueError('%s has no key set' % conf_key)
79 key = values.pop(0)
80 if values and '://' in values[-1]:
81 url = values.pop()
82 else:
83 url = 'https://' if 'cert_file' in conf else 'http://'
84 ip = conf.get('bind_ip', '127.0.0.1')
85 if ip == '0.0.0.0':
86 ip = '127.0.0.1'
87 url += ip
88 url += ':' + conf.get('bind_port', 80) + '/v1/' + \
89 self.reseller_prefix + conf_key.split('_')[1]
90 groups = values
91 self.users[conf_key.split('_', 1)[1].replace('_', ':')] = {
92 'key': key, 'url': url, 'groups': values}
93 self.created_accounts = False
94
95 def __call__(self, env, start_response):
96 """
97 Accepts a standard WSGI application call, authenticating the request
98 and installing callback hooks for authorization and ACL header
99 validation. For an authenticated request, REMOTE_USER will be set to a
100 comma separated list of the user's groups.
101
102 With a non-empty reseller prefix, acts as the definitive auth service
103 for just tokens and accounts that begin with that prefix, but will deny
104 requests outside this prefix if no other auth middleware overrides it.
105
106 With an empty reseller prefix, acts as the definitive auth service only
107 for tokens that validate to a non-empty set of groups. For all other
108 requests, acts as the fallback auth service when no other auth
109 middleware overrides it.
110
111 Alternatively, if the request matches the self.auth_prefix, the request
112 will be routed through the internal auth request handler (self.handle).
113 This is to handle granting tokens, etc.
114 """
115 # Ensure the accounts we handle have been created
116 if not self.created_accounts and self.users:
117 newenv = {'REQUEST_METHOD': 'HEAD', 'HTTP_USER_AGENT': 'TempAuth'}
118 for name in ('swift.cache', 'HTTP_X_TRANS_ID'):
119 if name in env:
120 newenv[name] = env[name]
121 for key, value in self.users.iteritems():
122 account_id = value['url'].rsplit('/', 1)[-1]
123 newenv['REQUEST_METHOD'] = 'HEAD'
124 resp = Request.blank('/v1/' + account_id,
125 environ=newenv).get_response(self.app)
126 if resp.status_int // 100 != 2:
127 newenv['REQUEST_METHOD'] = 'PUT'
128 resp = Request.blank('/v1/' + account_id,
129 environ=newenv).get_response(self.app)
130 if resp.status_int // 100 != 2:
131 raise Exception('Could not create account %s for user '
132 '%s' % (account_id, key))
133 self.created_accounts = True
134
135 if env.get('PATH_INFO', '').startswith(self.auth_prefix):
136 return self.handle(env, start_response)
137 s3 = env.get('HTTP_AUTHORIZATION')
138 token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN'))
139 if s3 or (token and token.startswith(self.reseller_prefix)):
140 # Note: Empty reseller_prefix will match all tokens.
141 groups = self.get_groups(env, token)
142 if groups:
143 env['REMOTE_USER'] = groups
144 user = groups and groups.split(',', 1)[0] or ''
145 # We know the proxy logs the token, so we augment it just a bit
146 # to also log the authenticated user.
147 env['HTTP_X_AUTH_TOKEN'] = \
148 '%s,%s' % (user, 's3' if s3 else token)
149 env['swift.authorize'] = self.authorize
150 env['swift.clean_acl'] = clean_acl
151 else:
152 # Unauthorized token
153 if self.reseller_prefix:
154 # Because I know I'm the definitive auth for this token, I
155 # can deny it outright.
156 return HTTPUnauthorized()(env, start_response)
157 # Because I'm not certain if I'm the definitive auth for empty
158 # reseller_prefixed tokens, I won't overwrite swift.authorize.
159 elif 'swift.authorize' not in env:
160 env['swift.authorize'] = self.denied_response
161 else:
162 if self.reseller_prefix:
163 # With a non-empty reseller_prefix, I would like to be called
164 # back for anonymous access to accounts I know I'm the
165 # definitive auth for.
166 try:
167 version, rest = split_path(env.get('PATH_INFO', ''),
168 1, 2, True)
169 except ValueError:
170 return HTTPNotFound()(env, start_response)
171 if rest and rest.startswith(self.reseller_prefix):
172 # Handle anonymous access to accounts I'm the definitive
173 # auth for.
174 env['swift.authorize'] = self.authorize
175 env['swift.clean_acl'] = clean_acl
176 # Not my token, not my account, I can't authorize this request,
177 # deny all is a good idea if not already set...
178 elif 'swift.authorize' not in env:
179 env['swift.authorize'] = self.denied_response
180 # Because I'm not certain if I'm the definitive auth for empty
181 # reseller_prefixed accounts, I won't overwrite swift.authorize.
182 elif 'swift.authorize' not in env:
183 env['swift.authorize'] = self.authorize
184 env['swift.clean_acl'] = clean_acl
185 return self.app(env, start_response)
186
187 def get_groups(self, env, token):
188 """
189 Get groups for the given token.
190
191 :param env: The current WSGI environment dictionary.
192 :param token: Token to validate and return a group string for.
193
194 :returns: None if the token is invalid or a string containing a comma
195 separated list of groups the authenticated user is a member
196 of. The first group in the list is also considered a unique
197 identifier for that user.
198 """
199 groups = None
200 memcache_client = cache_from_env(env)
201 if not memcache_client:
202 raise Exception('Memcache required')
203 memcache_token_key = '%s/token/%s' % (self.reseller_prefix, token)
204 cached_auth_data = memcache_client.get(memcache_token_key)
205 if cached_auth_data:
206 expires, groups = cached_auth_data
207 if expires < time():
208 groups = None
209
210 if env.get('HTTP_AUTHORIZATION'):
211 account_user, sign = \
212 env['HTTP_AUTHORIZATION'].split(' ')[1].rsplit(':', 1)
213 if account_user not in self.users:
214 return None
215 account, user = account_user.split(':', 1)
216 account_id = self.users[account_user]['url'].rsplit('/', 1)[-1]
217 path = env['PATH_INFO']
218 env['PATH_INFO'] = path.replace(account_user, account_id, 1)
219 msg = base64.urlsafe_b64decode(unquote(token))
220 key = self.users[account_user]['key']
221 s = base64.encodestring(hmac.new(key, msg, sha1).digest()).strip()
222 if s != sign:
223 return None
224 groups = [account, account_user]
225 groups.extend(self.users[account_user]['groups'])
226 if '.admin' in groups:
227 groups.remove('.admin')
228 groups.append(account_id)
229 groups = ','.join(groups)
230
231 return groups
232
233 def authorize(self, req):
234 """
235 Returns None if the request is authorized to continue or a standard
236 WSGI response callable if not.
237 """
238 try:
239 version, account, container, obj = split_path(req.path, 1, 4, True)
240 except ValueError:
241 return HTTPNotFound(request=req)
242 if not account or not account.startswith(self.reseller_prefix):
243 return self.denied_response(req)
244 user_groups = (req.remote_user or '').split(',')
245 if '.reseller_admin' in user_groups and \
246 account != self.reseller_prefix and \
247 account[len(self.reseller_prefix)] != '.':
248 return None
249 if account in user_groups and \
250 (req.method not in ('DELETE', 'PUT') or container):
251 # If the user is admin for the account and is not trying to do an
252 # account DELETE or PUT...
253 return None
254 referrers, groups = parse_acl(getattr(req, 'acl', None))
255 if referrer_allowed(req.referer, referrers):
256 if obj or '.rlistings' in groups:
257 return None
258 return self.denied_response(req)
259 if not req.remote_user:
260 return self.denied_response(req)
261 for user_group in user_groups:
262 if user_group in groups:
263 return None
264 return self.denied_response(req)
265
266 def denied_response(self, req):
267 """
268 Returns a standard WSGI response callable with the status of 403 or 401
269 depending on whether the REMOTE_USER is set or not.
270 """
271 if req.remote_user:
272 return HTTPForbidden(request=req)
273 else:
274 return HTTPUnauthorized(request=req)
275
276 def handle(self, env, start_response):
277 """
278 WSGI entry point for auth requests (ones that match the
279 self.auth_prefix).
280 Wraps env in webob.Request object and passes it down.
281
282 :param env: WSGI environment dictionary
283 :param start_response: WSGI callable
284 """
285 try:
286 req = Request(env)
287 if self.auth_prefix:
288 req.path_info_pop()
289 req.bytes_transferred = '-'
290 req.client_disconnect = False
291 if 'x-storage-token' in req.headers and \
292 'x-auth-token' not in req.headers:
293 req.headers['x-auth-token'] = req.headers['x-storage-token']
294 if 'eventlet.posthooks' in env:
295 env['eventlet.posthooks'].append(
296 (self.posthooklogger, (req,), {}))
297 return self.handle_request(req)(env, start_response)
298 else:
299 # Lack of posthook support means that we have to log on the
300 # start of the response, rather than after all the data has
301 # been sent. This prevents logging client disconnects
302 # differently than full transmissions.
303 response = self.handle_request(req)(env, start_response)
304 self.posthooklogger(env, req)
305 return response
306 except (Exception, TimeoutError):
307 print "EXCEPTION IN handle: %s: %s" % (format_exc(), env)
308 start_response('500 Server Error',
309 [('Content-Type', 'text/plain')])
310 return ['Internal server error.\n']
311
312 def handle_request(self, req):
313 """
314 Entry point for auth requests (ones that match the self.auth_prefix).
315 Should return a WSGI-style callable (such as webob.Response).
316
317 :param req: webob.Request object
318 """
319 req.start_time = time()
320 handler = None
321 try:
322 version, account, user, _junk = split_path(req.path_info,
323 minsegs=1, maxsegs=4, rest_with_last=True)
324 except ValueError:
325 return HTTPNotFound(request=req)
326 if version in ('v1', 'v1.0', 'auth'):
327 if req.method == 'GET':
328 handler = self.handle_get_token
329 if not handler:
330 req.response = HTTPBadRequest(request=req)
331 else:
332 req.response = handler(req)
333 return req.response
334
335 def handle_get_token(self, req):
336 """
337 Handles the various `request for token and service end point(s)` calls.
338 There are various formats to support the various auth servers in the
339 past. Examples::
340
341 GET <auth-prefix>/v1/<act>/auth
342 X-Auth-User: <act>:<usr> or X-Storage-User: <usr>
343 X-Auth-Key: <key> or X-Storage-Pass: <key>
344 GET <auth-prefix>/auth
345 X-Auth-User: <act>:<usr> or X-Storage-User: <act>:<usr>
346 X-Auth-Key: <key> or X-Storage-Pass: <key>
347 GET <auth-prefix>/v1.0
348 X-Auth-User: <act>:<usr> or X-Storage-User: <act>:<usr>
349 X-Auth-Key: <key> or X-Storage-Pass: <key>
350
351 On successful authentication, the response will have X-Auth-Token and
352 X-Storage-Token set to the token to use with Swift and X-Storage-URL
353 set to the URL to the default Swift cluster to use.
354
355 :param req: The webob.Request to process.
356 :returns: webob.Response, 2xx on success with data set as explained
357 above.
358 """
359 # Validate the request info
360 try:
361 pathsegs = split_path(req.path_info, minsegs=1, maxsegs=3,
362 rest_with_last=True)
363 except ValueError:
364 return HTTPNotFound(request=req)
365 if pathsegs[0] == 'v1' and pathsegs[2] == 'auth':
366 account = pathsegs[1]
367 user = req.headers.get('x-storage-user')
368 if not user:
369 user = req.headers.get('x-auth-user')
370 if not user or ':' not in user:
371 return HTTPUnauthorized(request=req)
372 account2, user = user.split(':', 1)
373 if account != account2:
374 return HTTPUnauthorized(request=req)
375 key = req.headers.get('x-storage-pass')
376 if not key:
377 key = req.headers.get('x-auth-key')
378 elif pathsegs[0] in ('auth', 'v1.0'):
379 user = req.headers.get('x-auth-user')
380 if not user:
381 user = req.headers.get('x-storage-user')
382 if not user or ':' not in user:
383 return HTTPUnauthorized(request=req)
384 account, user = user.split(':', 1)
385 key = req.headers.get('x-auth-key')
386 if not key:
387 key = req.headers.get('x-storage-pass')
388 else:
389 return HTTPBadRequest(request=req)
390 if not all((account, user, key)):
391 return HTTPUnauthorized(request=req)
392 # Authenticate user
393 account_user = account + ':' + user
394 if account_user not in self.users:
395 return HTTPUnauthorized(request=req)
396 if self.users[account_user]['key'] != key:
397 return HTTPUnauthorized(request=req)
398 # Get memcache client
399 memcache_client = cache_from_env(req.environ)
400 if not memcache_client:
401 raise Exception('Memcache required')
402 # See if a token already exists and hasn't expired
403 token = None
404 memcache_user_key = '%s/user/%s' % (self.reseller_prefix, account_user)
405 candidate_token = memcache_client.get(memcache_user_key)
406 if candidate_token:
407 memcache_token_key = \
408 '%s/token/%s' % (self.reseller_prefix, candidate_token)
409 cached_auth_data = memcache_client.get(memcache_token_key)
410 if cached_auth_data:
411 expires, groups = cached_auth_data
412 if expires > time():
413 token = candidate_token
414 # Create a new token if one didn't exist
415 if not token:
416 # Generate new token
417 token = '%stk%s' % (self.reseller_prefix, uuid4().hex)
418 expires = time() + self.token_life
419 groups = [account, account_user]
420 groups.extend(self.users[account_user]['groups'])
421 if '.admin' in groups:
422 groups.remove('.admin')
423 account_id = self.users[account_user]['url'].rsplit('/', 1)[-1]
424 groups.append(account_id)
425 groups = ','.join(groups)
426 # Save token
427 memcache_token_key = '%s/token/%s' % (self.reseller_prefix, token)
428 memcache_client.set(memcache_token_key, (expires, groups),
429 timeout=float(expires - time()))
430 # Record the token with the user info for future use.
431 memcache_user_key = \
432 '%s/user/%s' % (self.reseller_prefix, account_user)
433 memcache_client.set(memcache_user_key, token,
434 timeout=float(expires - time()))
435 return Response(request=req,
436 headers={'x-auth-token': token, 'x-storage-token': token,
437 'x-storage-url': self.users[account_user]['url']})
438
439 def posthooklogger(self, env, req):
440 if not req.path.startswith(self.auth_prefix):
441 return
442 response = getattr(req, 'response', None)
443 if not response:
444 return
445 trans_time = '%.4f' % (time() - req.start_time)
446 the_request = quote(unquote(req.path))
447 if req.query_string:
448 the_request = the_request + '?' + req.query_string
449 # remote user for zeus
450 client = req.headers.get('x-cluster-client-ip')
451 if not client and 'x-forwarded-for' in req.headers:
452 # remote user for other lbs
453 client = req.headers['x-forwarded-for'].split(',')[0].strip()
454 logged_headers = None
455 if self.log_headers:
456 logged_headers = '\n'.join('%s: %s' % (k, v)
457 for k, v in req.headers.items())
458 status_int = response.status_int
459 if getattr(req, 'client_disconnect', False) or \
460 getattr(response, 'client_disconnect', False):
461 status_int = 499
462 self.logger.info(' '.join(quote(str(x)) for x in (client or '-',
463 req.remote_addr or '-', strftime('%d/%b/%Y/%H/%M/%S', gmtime()),
464 req.method, the_request, req.environ['SERVER_PROTOCOL'],
465 status_int, req.referer or '-', req.user_agent or '-',
466 req.headers.get('x-auth-token',
467 req.headers.get('x-auth-admin-user', '-')),
468 getattr(req, 'bytes_transferred', 0) or '-',
469 getattr(response, 'bytes_transferred', 0) or '-',
470 req.headers.get('etag', '-'),
471 req.headers.get('x-trans-id', '-'), logged_headers or '-',
472 trans_time)))
473
474
475def filter_factory(global_conf, **local_conf):
476 """Returns a WSGI filter app for use with paste.deploy."""
477 conf = global_conf.copy()
478 conf.update(local_conf)
479
480 def auth_filter(app):
481 return TempAuth(app, conf)
482 return auth_filter
0483
=== modified file 'test/probe/common.py'
--- test/probe/common.py 2011-03-14 02:56:37 +0000
+++ test/probe/common.py 2011-06-09 21:09:39 +0000
@@ -13,29 +13,16 @@
13# See the License for the specific language governing permissions and13# See the License for the specific language governing permissions and
14# limitations under the License.14# limitations under the License.
1515
16from os import environ, kill16from os import kill
17from signal import SIGTERM17from signal import SIGTERM
18from subprocess import call, Popen18from subprocess import call, Popen
19from time import sleep19from time import sleep
20from ConfigParser import ConfigParser
2120
22from swift.common.bufferedhttp import http_connect_raw as http_connect21from swift.common.bufferedhttp import http_connect_raw as http_connect
23from swift.common.client import get_auth22from swift.common.client import get_auth
24from swift.common.ring import Ring23from swift.common.ring import Ring
2524
2625
27SUPER_ADMIN_KEY = None
28
29c = ConfigParser()
30PROXY_SERVER_CONF_FILE = environ.get('SWIFT_PROXY_SERVER_CONF_FILE',
31 '/etc/swift/proxy-server.conf')
32if c.read(PROXY_SERVER_CONF_FILE):
33 conf = dict(c.items('filter:swauth'))
34 SUPER_ADMIN_KEY = conf.get('super_admin_key', 'swauthkey')
35else:
36 exit('Unable to read config file: %s' % PROXY_SERVER_CONF_FILE)
37
38
39def kill_pids(pids):26def kill_pids(pids):
40 for pid in pids.values():27 for pid in pids.values():
41 try:28 try:
@@ -48,8 +35,6 @@
48 call(['resetswift'])35 call(['resetswift'])
49 pids = {}36 pids = {}
50 try:37 try:
51 pids['proxy'] = Popen(['swift-proxy-server',
52 '/etc/swift/proxy-server.conf']).pid
53 port2server = {}38 port2server = {}
54 for s, p in (('account', 6002), ('container', 6001), ('object', 6000)):39 for s, p in (('account', 6002), ('container', 6001), ('object', 6000)):
55 for n in xrange(1, 5):40 for n in xrange(1, 5):
@@ -57,14 +42,27 @@
57 Popen(['swift-%s-server' % s,42 Popen(['swift-%s-server' % s,
58 '/etc/swift/%s-server/%d.conf' % (s, n)]).pid43 '/etc/swift/%s-server/%d.conf' % (s, n)]).pid
59 port2server[p + (n * 10)] = '%s%d' % (s, n)44 port2server[p + (n * 10)] = '%s%d' % (s, n)
45 pids['proxy'] = Popen(['swift-proxy-server',
46 '/etc/swift/proxy-server.conf']).pid
60 account_ring = Ring('/etc/swift/account.ring.gz')47 account_ring = Ring('/etc/swift/account.ring.gz')
61 container_ring = Ring('/etc/swift/container.ring.gz')48 container_ring = Ring('/etc/swift/container.ring.gz')
62 object_ring = Ring('/etc/swift/object.ring.gz')49 object_ring = Ring('/etc/swift/object.ring.gz')
63 sleep(5)50 attempt = 0
64 call(['recreateaccounts'])51 while True:
65 url, token = get_auth('http://127.0.0.1:8080/auth/v1.0',52 attempt += 1
66 'test:tester', 'testing')53 try:
67 account = url.split('/')[-1]54 url, token = get_auth('http://127.0.0.1:8080/auth/v1.0',
55 'test:tester', 'testing')
56 account = url.split('/')[-1]
57 break
58 except Exception, err:
59 if attempt > 9:
60 print err
61 print 'Giving up after %s retries.' % attempt
62 raise err
63 print err
64 print 'Retrying in 1 second...'
65 sleep(1)
68 except BaseException, err:66 except BaseException, err:
69 kill_pids(pids)67 kill_pids(pids)
70 raise err68 raise err
7169
=== renamed file 'test/unit/common/middleware/test_swauth.py' => 'test/unit/common/middleware/test_tempauth.py'
--- test/unit/common/middleware/test_swauth.py 2011-04-01 21:17:47 +0000
+++ test/unit/common/middleware/test_tempauth.py 2011-06-09 21:09:39 +0000
@@ -1,4 +1,4 @@
1# Copyright (c) 2010 OpenStack, LLC.1# Copyright (c) 2011 OpenStack, LLC.
2#2#
3# Licensed under the Apache License, Version 2.0 (the "License");3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.4# you may not use this file except in compliance with the License.
@@ -23,7 +23,7 @@
2323
24from webob import Request, Response24from webob import Request, Response
2525
26from swift.common.middleware import swauth as auth26from swift.common.middleware import tempauth as auth
2727
2828
29class FakeMemcache(object):29class FakeMemcache(object):
@@ -102,85 +102,49 @@
102class TestAuth(unittest.TestCase):102class TestAuth(unittest.TestCase):
103103
104 def setUp(self):104 def setUp(self):
105 self.test_auth = \105 self.test_auth = auth.filter_factory({})(FakeApp())
106 auth.filter_factory({'super_admin_key': 'supertest'})(FakeApp())
107106
108 def test_super_admin_key_required(self):107 def _make_request(self, path, **kwargs):
109 app = FakeApp()108 req = Request.blank(path, **kwargs)
110 exc = None109 req.environ['swift.cache'] = FakeMemcache()
111 try:110 return req
112 auth.filter_factory({})(app)
113 except ValueError, err:
114 exc = err
115 self.assertEquals(str(exc),
116 'No super_admin_key set in conf file! Exiting.')
117 auth.filter_factory({'super_admin_key': 'supertest'})(app)
118111
119 def test_reseller_prefix_init(self):112 def test_reseller_prefix_init(self):
120 app = FakeApp()113 app = FakeApp()
121 ath = auth.filter_factory({'super_admin_key': 'supertest'})(app)114 ath = auth.filter_factory({})(app)
122 self.assertEquals(ath.reseller_prefix, 'AUTH_')115 self.assertEquals(ath.reseller_prefix, 'AUTH_')
123 ath = auth.filter_factory({'super_admin_key': 'supertest',116 ath = auth.filter_factory({'reseller_prefix': 'TEST'})(app)
124 'reseller_prefix': 'TEST'})(app)
125 self.assertEquals(ath.reseller_prefix, 'TEST_')117 self.assertEquals(ath.reseller_prefix, 'TEST_')
126 ath = auth.filter_factory({'super_admin_key': 'supertest',118 ath = auth.filter_factory({'reseller_prefix': 'TEST_'})(app)
127 'reseller_prefix': 'TEST_'})(app)
128 self.assertEquals(ath.reseller_prefix, 'TEST_')119 self.assertEquals(ath.reseller_prefix, 'TEST_')
129120
130 def test_auth_prefix_init(self):121 def test_auth_prefix_init(self):
131 app = FakeApp()122 app = FakeApp()
132 ath = auth.filter_factory({'super_admin_key': 'supertest'})(app)123 ath = auth.filter_factory({})(app)
133 self.assertEquals(ath.auth_prefix, '/auth/')124 self.assertEquals(ath.auth_prefix, '/auth/')
134 ath = auth.filter_factory({'super_admin_key': 'supertest',125 ath = auth.filter_factory({'auth_prefix': ''})(app)
135 'auth_prefix': ''})(app)126 self.assertEquals(ath.auth_prefix, '/auth/')
136 self.assertEquals(ath.auth_prefix, '/auth/')127 ath = auth.filter_factory({'auth_prefix': '/test/'})(app)
137 ath = auth.filter_factory({'super_admin_key': 'supertest',128 self.assertEquals(ath.auth_prefix, '/test/')
138 'auth_prefix': '/test/'})(app)129 ath = auth.filter_factory({'auth_prefix': '/test'})(app)
139 self.assertEquals(ath.auth_prefix, '/test/')130 self.assertEquals(ath.auth_prefix, '/test/')
140 ath = auth.filter_factory({'super_admin_key': 'supertest',131 ath = auth.filter_factory({'auth_prefix': 'test/'})(app)
141 'auth_prefix': '/test'})(app)132 self.assertEquals(ath.auth_prefix, '/test/')
142 self.assertEquals(ath.auth_prefix, '/test/')133 ath = auth.filter_factory({'auth_prefix': 'test'})(app)
143 ath = auth.filter_factory({'super_admin_key': 'supertest',134 self.assertEquals(ath.auth_prefix, '/test/')
144 'auth_prefix': 'test/'})(app)
145 self.assertEquals(ath.auth_prefix, '/test/')
146 ath = auth.filter_factory({'super_admin_key': 'supertest',
147 'auth_prefix': 'test'})(app)
148 self.assertEquals(ath.auth_prefix, '/test/')
149
150 def test_default_swift_cluster_init(self):
151 app = FakeApp()
152 self.assertRaises(Exception, auth.filter_factory({
153 'super_admin_key': 'supertest',
154 'default_swift_cluster': 'local#badscheme://host/path'}), app)
155 ath = auth.filter_factory({'super_admin_key': 'supertest'})(app)
156 self.assertEquals(ath.default_swift_cluster,
157 'local#http://127.0.0.1:8080/v1')
158 ath = auth.filter_factory({'super_admin_key': 'supertest',
159 'default_swift_cluster': 'local#http://host/path'})(app)
160 self.assertEquals(ath.default_swift_cluster,
161 'local#http://host/path')
162 ath = auth.filter_factory({'super_admin_key': 'supertest',
163 'default_swift_cluster': 'local#https://host/path/'})(app)
164 self.assertEquals(ath.dsc_url, 'https://host/path')
165 self.assertEquals(ath.dsc_url2, 'https://host/path')
166 ath = auth.filter_factory({'super_admin_key': 'supertest',
167 'default_swift_cluster':
168 'local#https://host/path/#http://host2/path2/'})(app)
169 self.assertEquals(ath.dsc_url, 'https://host/path')
170 self.assertEquals(ath.dsc_url2, 'http://host2/path2')
171135
172 def test_top_level_ignore(self):136 def test_top_level_ignore(self):
173 resp = Request.blank('/').get_response(self.test_auth)137 resp = self._make_request('/').get_response(self.test_auth)
174 self.assertEquals(resp.status_int, 404)138 self.assertEquals(resp.status_int, 404)
175139
176 def test_anon(self):140 def test_anon(self):
177 resp = Request.blank('/v1/AUTH_account').get_response(self.test_auth)141 resp = self._make_request('/v1/AUTH_account').get_response(self.test_auth)
178 self.assertEquals(resp.status_int, 401)142 self.assertEquals(resp.status_int, 401)
179 self.assertEquals(resp.environ['swift.authorize'],143 self.assertEquals(resp.environ['swift.authorize'],
180 self.test_auth.authorize)144 self.test_auth.authorize)
181145
182 def test_auth_deny_non_reseller_prefix(self):146 def test_auth_deny_non_reseller_prefix(self):
183 resp = Request.blank('/v1/BLAH_account',147 resp = self._make_request('/v1/BLAH_account',
184 headers={'X-Auth-Token': 'BLAH_t'}).get_response(self.test_auth)148 headers={'X-Auth-Token': 'BLAH_t'}).get_response(self.test_auth)
185 self.assertEquals(resp.status_int, 401)149 self.assertEquals(resp.status_int, 401)
186 self.assertEquals(resp.environ['swift.authorize'],150 self.assertEquals(resp.environ['swift.authorize'],
@@ -188,7 +152,7 @@
188152
189 def test_auth_deny_non_reseller_prefix_no_override(self):153 def test_auth_deny_non_reseller_prefix_no_override(self):
190 fake_authorize = lambda x: Response(status='500 Fake')154 fake_authorize = lambda x: Response(status='500 Fake')
191 resp = Request.blank('/v1/BLAH_account',155 resp = self._make_request('/v1/BLAH_account',
192 headers={'X-Auth-Token': 'BLAH_t'},156 headers={'X-Auth-Token': 'BLAH_t'},
193 environ={'swift.authorize': fake_authorize}157 environ={'swift.authorize': fake_authorize}
194 ).get_response(self.test_auth)158 ).get_response(self.test_auth)
@@ -200,192 +164,74 @@
200 # outright but set up a denial swift.authorize and pass the request on164 # outright but set up a denial swift.authorize and pass the request on
201 # down the chain.165 # down the chain.
202 local_app = FakeApp()166 local_app = FakeApp()
203 local_auth = auth.filter_factory({'super_admin_key': 'supertest',167 local_auth = auth.filter_factory({'reseller_prefix': ''})(local_app)
204 'reseller_prefix': ''})(local_app)168 resp = self._make_request('/v1/account',
205 resp = Request.blank('/v1/account',
206 headers={'X-Auth-Token': 't'}).get_response(local_auth)169 headers={'X-Auth-Token': 't'}).get_response(local_auth)
207 self.assertEquals(resp.status_int, 401)170 self.assertEquals(resp.status_int, 401)
208 # one for checking auth, two for request passed along171 self.assertEquals(local_app.calls, 1)
209 self.assertEquals(local_app.calls, 2)
210 self.assertEquals(resp.environ['swift.authorize'],172 self.assertEquals(resp.environ['swift.authorize'],
211 local_auth.denied_response)173 local_auth.denied_response)
212174
213 def test_auth_no_reseller_prefix_allow(self):
214 # Ensures that when we have no reseller prefix, we can still allow
215 # access if our auth server accepts requests
216 local_app = FakeApp(iter([
217 ('200 Ok', {},
218 json.dumps({'account': 'act', 'user': 'act:usr',
219 'account_id': 'AUTH_cfa',
220 'groups': [{'name': 'act:usr'}, {'name': 'act'},
221 {'name': '.admin'}],
222 'expires': time() + 60})),
223 ('204 No Content', {}, '')]))
224 local_auth = auth.filter_factory({'super_admin_key': 'supertest',
225 'reseller_prefix': ''})(local_app)
226 resp = Request.blank('/v1/act',
227 headers={'X-Auth-Token': 't'}).get_response(local_auth)
228 self.assertEquals(resp.status_int, 204)
229 self.assertEquals(local_app.calls, 2)
230 self.assertEquals(resp.environ['swift.authorize'],
231 local_auth.authorize)
232
233 def test_auth_no_reseller_prefix_no_token(self):175 def test_auth_no_reseller_prefix_no_token(self):
234 # Check that normally we set up a call back to our authorize.176 # Check that normally we set up a call back to our authorize.
235 local_auth = \177 local_auth = \
236 auth.filter_factory({'super_admin_key': 'supertest',178 auth.filter_factory({'reseller_prefix': ''})(FakeApp(iter([])))
237 'reseller_prefix': ''})(FakeApp(iter([])))179 resp = self._make_request('/v1/account').get_response(local_auth)
238 resp = Request.blank('/v1/account').get_response(local_auth)
239 self.assertEquals(resp.status_int, 401)180 self.assertEquals(resp.status_int, 401)
240 self.assertEquals(resp.environ['swift.authorize'],181 self.assertEquals(resp.environ['swift.authorize'],
241 local_auth.authorize)182 local_auth.authorize)
242 # Now make sure we don't override an existing swift.authorize when we183 # Now make sure we don't override an existing swift.authorize when we
243 # have no reseller prefix.184 # have no reseller prefix.
244 local_auth = \185 local_auth = \
245 auth.filter_factory({'super_admin_key': 'supertest',186 auth.filter_factory({'reseller_prefix': ''})(FakeApp())
246 'reseller_prefix': ''})(FakeApp())
247 local_authorize = lambda req: Response('test')187 local_authorize = lambda req: Response('test')
248 resp = Request.blank('/v1/account', environ={'swift.authorize':188 resp = self._make_request('/v1/account', environ={'swift.authorize':
249 local_authorize}).get_response(local_auth)189 local_authorize}).get_response(local_auth)
250 self.assertEquals(resp.status_int, 200)190 self.assertEquals(resp.status_int, 200)
251 self.assertEquals(resp.environ['swift.authorize'], local_authorize)191 self.assertEquals(resp.environ['swift.authorize'], local_authorize)
252192
253 def test_auth_fail(self):193 def test_auth_fail(self):
254 resp = Request.blank('/v1/AUTH_cfa',194 resp = self._make_request('/v1/AUTH_cfa',
255 headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth)195 headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth)
256 self.assertEquals(resp.status_int, 401)196 self.assertEquals(resp.status_int, 401)
257
258 def test_auth_success(self):
259 self.test_auth.app = FakeApp(iter([
260 ('200 Ok', {},
261 json.dumps({'account': 'act', 'user': 'act:usr',
262 'account_id': 'AUTH_cfa',
263 'groups': [{'name': 'act:usr'}, {'name': 'act'},
264 {'name': '.admin'}],
265 'expires': time() + 60})),
266 ('204 No Content', {}, '')]))
267 resp = Request.blank('/v1/AUTH_cfa',
268 headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth)
269 self.assertEquals(resp.status_int, 204)
270 self.assertEquals(self.test_auth.app.calls, 2)
271
272 def test_auth_memcache(self):
273 # First run our test without memcache, showing we need to return the
274 # token contents twice.
275 self.test_auth.app = FakeApp(iter([
276 ('200 Ok', {},
277 json.dumps({'account': 'act', 'user': 'act:usr',
278 'account_id': 'AUTH_cfa',
279 'groups': [{'name': 'act:usr'}, {'name': 'act'},
280 {'name': '.admin'}],
281 'expires': time() + 60})),
282 ('204 No Content', {}, ''),
283 ('200 Ok', {},
284 json.dumps({'account': 'act', 'user': 'act:usr',
285 'account_id': 'AUTH_cfa',
286 'groups': [{'name': 'act:usr'}, {'name': 'act'},
287 {'name': '.admin'}],
288 'expires': time() + 60})),
289 ('204 No Content', {}, '')]))
290 resp = Request.blank('/v1/AUTH_cfa',
291 headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth)
292 self.assertEquals(resp.status_int, 204)
293 resp = Request.blank('/v1/AUTH_cfa',
294 headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth)
295 self.assertEquals(resp.status_int, 204)
296 self.assertEquals(self.test_auth.app.calls, 4)
297 # Now run our test with memcache, showing we no longer need to return
298 # the token contents twice.
299 self.test_auth.app = FakeApp(iter([
300 ('200 Ok', {},
301 json.dumps({'account': 'act', 'user': 'act:usr',
302 'account_id': 'AUTH_cfa',
303 'groups': [{'name': 'act:usr'}, {'name': 'act'},
304 {'name': '.admin'}],
305 'expires': time() + 60})),
306 ('204 No Content', {}, ''),
307 # Don't need a second token object returned if memcache is used
308 ('204 No Content', {}, '')]))
309 fake_memcache = FakeMemcache()
310 resp = Request.blank('/v1/AUTH_cfa',
311 headers={'X-Auth-Token': 'AUTH_t'},
312 environ={'swift.cache': fake_memcache}
313 ).get_response(self.test_auth)
314 self.assertEquals(resp.status_int, 204)
315 resp = Request.blank('/v1/AUTH_cfa',
316 headers={'X-Auth-Token': 'AUTH_t'},
317 environ={'swift.cache': fake_memcache}
318 ).get_response(self.test_auth)
319 self.assertEquals(resp.status_int, 204)
320 self.assertEquals(self.test_auth.app.calls, 3)
321
322 def test_auth_just_expired(self):
323 self.test_auth.app = FakeApp(iter([
324 # Request for token (which will have expired)
325 ('200 Ok', {},
326 json.dumps({'account': 'act', 'user': 'act:usr',
327 'account_id': 'AUTH_cfa',
328 'groups': [{'name': 'act:usr'}, {'name': 'act'},
329 {'name': '.admin'}],
330 'expires': time() - 1})),
331 # Request to delete token
332 ('204 No Content', {}, '')]))
333 resp = Request.blank('/v1/AUTH_cfa',
334 headers={'X-Auth-Token': 'AUTH_t'}).get_response(self.test_auth)
335 self.assertEquals(resp.status_int, 401)
336 self.assertEquals(self.test_auth.app.calls, 2)
337
338 def test_middleware_storage_token(self):
339 self.test_auth.app = FakeApp(iter([
340 ('200 Ok', {},
341 json.dumps({'account': 'act', 'user': 'act:usr',
342 'account_id': 'AUTH_cfa',
343 'groups': [{'name': 'act:usr'}, {'name': 'act'},
344 {'name': '.admin'}],
345 'expires': time() + 60})),
346 ('204 No Content', {}, '')]))
347 resp = Request.blank('/v1/AUTH_cfa',
348 headers={'X-Storage-Token': 'AUTH_t'}).get_response(self.test_auth)
349 self.assertEquals(resp.status_int, 204)
350 self.assertEquals(self.test_auth.app.calls, 2)
351197
352 def test_authorize_bad_path(self):198 def test_authorize_bad_path(self):
353 req = Request.blank('/badpath')199 req = self._make_request('/badpath')
354 resp = self.test_auth.authorize(req)200 resp = self.test_auth.authorize(req)
355 self.assertEquals(resp.status_int, 401)201 self.assertEquals(resp.status_int, 401)
356 req = Request.blank('/badpath')202 req = self._make_request('/badpath')
357 req.remote_user = 'act:usr,act,AUTH_cfa'203 req.remote_user = 'act:usr,act,AUTH_cfa'
358 resp = self.test_auth.authorize(req)204 resp = self.test_auth.authorize(req)
359 self.assertEquals(resp.status_int, 403)205 self.assertEquals(resp.status_int, 403)
360206
361 def test_authorize_account_access(self):207 def test_authorize_account_access(self):
362 req = Request.blank('/v1/AUTH_cfa')208 req = self._make_request('/v1/AUTH_cfa')
363 req.remote_user = 'act:usr,act,AUTH_cfa'209 req.remote_user = 'act:usr,act,AUTH_cfa'
364 self.assertEquals(self.test_auth.authorize(req), None)210 self.assertEquals(self.test_auth.authorize(req), None)
365 req = Request.blank('/v1/AUTH_cfa')211 req = self._make_request('/v1/AUTH_cfa')
366 req.remote_user = 'act:usr,act'212 req.remote_user = 'act:usr,act'
367 resp = self.test_auth.authorize(req)213 resp = self.test_auth.authorize(req)
368 self.assertEquals(resp.status_int, 403)214 self.assertEquals(resp.status_int, 403)
369215
370 def test_authorize_acl_group_access(self):216 def test_authorize_acl_group_access(self):
371 req = Request.blank('/v1/AUTH_cfa')217 req = self._make_request('/v1/AUTH_cfa')
372 req.remote_user = 'act:usr,act'218 req.remote_user = 'act:usr,act'
373 resp = self.test_auth.authorize(req)219 resp = self.test_auth.authorize(req)
374 self.assertEquals(resp.status_int, 403)220 self.assertEquals(resp.status_int, 403)
375 req = Request.blank('/v1/AUTH_cfa')221 req = self._make_request('/v1/AUTH_cfa')
376 req.remote_user = 'act:usr,act'222 req.remote_user = 'act:usr,act'
377 req.acl = 'act'223 req.acl = 'act'
378 self.assertEquals(self.test_auth.authorize(req), None)224 self.assertEquals(self.test_auth.authorize(req), None)
379 req = Request.blank('/v1/AUTH_cfa')225 req = self._make_request('/v1/AUTH_cfa')
380 req.remote_user = 'act:usr,act'226 req.remote_user = 'act:usr,act'
381 req.acl = 'act:usr'227 req.acl = 'act:usr'
382 self.assertEquals(self.test_auth.authorize(req), None)228 self.assertEquals(self.test_auth.authorize(req), None)
383 req = Request.blank('/v1/AUTH_cfa')229 req = self._make_request('/v1/AUTH_cfa')
384 req.remote_user = 'act:usr,act'230 req.remote_user = 'act:usr,act'
385 req.acl = 'act2'231 req.acl = 'act2'
386 resp = self.test_auth.authorize(req)232 resp = self.test_auth.authorize(req)
387 self.assertEquals(resp.status_int, 403)233 self.assertEquals(resp.status_int, 403)
388 req = Request.blank('/v1/AUTH_cfa')234 req = self._make_request('/v1/AUTH_cfa')
389 req.remote_user = 'act:usr,act'235 req.remote_user = 'act:usr,act'
390 req.acl = 'act:usr2'236 req.acl = 'act:usr2'
391 resp = self.test_auth.authorize(req)237 resp = self.test_auth.authorize(req)
@@ -393,105 +239,105 @@
393239
394 def test_deny_cross_reseller(self):240 def test_deny_cross_reseller(self):
395 # Tests that cross-reseller is denied, even if ACLs/group names match241 # Tests that cross-reseller is denied, even if ACLs/group names match
396 req = Request.blank('/v1/OTHER_cfa')242 req = self._make_request('/v1/OTHER_cfa')
397 req.remote_user = 'act:usr,act,AUTH_cfa'243 req.remote_user = 'act:usr,act,AUTH_cfa'
398 req.acl = 'act'244 req.acl = 'act'
399 resp = self.test_auth.authorize(req)245 resp = self.test_auth.authorize(req)
400 self.assertEquals(resp.status_int, 403)246 self.assertEquals(resp.status_int, 403)
401247
402 def test_authorize_acl_referrer_access(self):248 def test_authorize_acl_referrer_access(self):
403 req = Request.blank('/v1/AUTH_cfa/c')249 req = self._make_request('/v1/AUTH_cfa/c')
404 req.remote_user = 'act:usr,act'250 req.remote_user = 'act:usr,act'
405 resp = self.test_auth.authorize(req)251 resp = self.test_auth.authorize(req)
406 self.assertEquals(resp.status_int, 403)252 self.assertEquals(resp.status_int, 403)
407 req = Request.blank('/v1/AUTH_cfa/c')253 req = self._make_request('/v1/AUTH_cfa/c')
408 req.remote_user = 'act:usr,act'254 req.remote_user = 'act:usr,act'
409 req.acl = '.r:*,.rlistings'255 req.acl = '.r:*,.rlistings'
410 self.assertEquals(self.test_auth.authorize(req), None)256 self.assertEquals(self.test_auth.authorize(req), None)
411 req = Request.blank('/v1/AUTH_cfa/c')257 req = self._make_request('/v1/AUTH_cfa/c')
412 req.remote_user = 'act:usr,act'258 req.remote_user = 'act:usr,act'
413 req.acl = '.r:*' # No listings allowed259 req.acl = '.r:*' # No listings allowed
414 resp = self.test_auth.authorize(req)260 resp = self.test_auth.authorize(req)
415 self.assertEquals(resp.status_int, 403)261 self.assertEquals(resp.status_int, 403)
416 req = Request.blank('/v1/AUTH_cfa/c')262 req = self._make_request('/v1/AUTH_cfa/c')
417 req.remote_user = 'act:usr,act'263 req.remote_user = 'act:usr,act'
418 req.acl = '.r:.example.com,.rlistings'264 req.acl = '.r:.example.com,.rlistings'
419 resp = self.test_auth.authorize(req)265 resp = self.test_auth.authorize(req)
420 self.assertEquals(resp.status_int, 403)266 self.assertEquals(resp.status_int, 403)
421 req = Request.blank('/v1/AUTH_cfa/c')267 req = self._make_request('/v1/AUTH_cfa/c')
422 req.remote_user = 'act:usr,act'268 req.remote_user = 'act:usr,act'
423 req.referer = 'http://www.example.com/index.html'269 req.referer = 'http://www.example.com/index.html'
424 req.acl = '.r:.example.com,.rlistings'270 req.acl = '.r:.example.com,.rlistings'
425 self.assertEquals(self.test_auth.authorize(req), None)271 self.assertEquals(self.test_auth.authorize(req), None)
426 req = Request.blank('/v1/AUTH_cfa/c')272 req = self._make_request('/v1/AUTH_cfa/c')
427 resp = self.test_auth.authorize(req)273 resp = self.test_auth.authorize(req)
428 self.assertEquals(resp.status_int, 401)274 self.assertEquals(resp.status_int, 401)
429 req = Request.blank('/v1/AUTH_cfa/c')275 req = self._make_request('/v1/AUTH_cfa/c')
430 req.acl = '.r:*,.rlistings'276 req.acl = '.r:*,.rlistings'
431 self.assertEquals(self.test_auth.authorize(req), None)277 self.assertEquals(self.test_auth.authorize(req), None)
432 req = Request.blank('/v1/AUTH_cfa/c')278 req = self._make_request('/v1/AUTH_cfa/c')
433 req.acl = '.r:*' # No listings allowed279 req.acl = '.r:*' # No listings allowed
434 resp = self.test_auth.authorize(req)280 resp = self.test_auth.authorize(req)
435 self.assertEquals(resp.status_int, 401)281 self.assertEquals(resp.status_int, 401)
436 req = Request.blank('/v1/AUTH_cfa/c')282 req = self._make_request('/v1/AUTH_cfa/c')
437 req.acl = '.r:.example.com,.rlistings'283 req.acl = '.r:.example.com,.rlistings'
438 resp = self.test_auth.authorize(req)284 resp = self.test_auth.authorize(req)
439 self.assertEquals(resp.status_int, 401)285 self.assertEquals(resp.status_int, 401)
440 req = Request.blank('/v1/AUTH_cfa/c')286 req = self._make_request('/v1/AUTH_cfa/c')
441 req.referer = 'http://www.example.com/index.html'287 req.referer = 'http://www.example.com/index.html'
442 req.acl = '.r:.example.com,.rlistings'288 req.acl = '.r:.example.com,.rlistings'
443 self.assertEquals(self.test_auth.authorize(req), None)289 self.assertEquals(self.test_auth.authorize(req), None)
444290
445 def test_account_put_permissions(self):291 def test_account_put_permissions(self):
446 req = Request.blank('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'})292 req = self._make_request('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'})
447 req.remote_user = 'act:usr,act'293 req.remote_user = 'act:usr,act'
448 resp = self.test_auth.authorize(req)294 resp = self.test_auth.authorize(req)
449 self.assertEquals(resp.status_int, 403)295 self.assertEquals(resp.status_int, 403)
450296
451 req = Request.blank('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'})297 req = self._make_request('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'})
452 req.remote_user = 'act:usr,act,AUTH_other'298 req.remote_user = 'act:usr,act,AUTH_other'
453 resp = self.test_auth.authorize(req)299 resp = self.test_auth.authorize(req)
454 self.assertEquals(resp.status_int, 403)300 self.assertEquals(resp.status_int, 403)
455301
456 # Even PUTs to your own account as account admin should fail302 # Even PUTs to your own account as account admin should fail
457 req = Request.blank('/v1/AUTH_old', environ={'REQUEST_METHOD': 'PUT'})303 req = self._make_request('/v1/AUTH_old', environ={'REQUEST_METHOD': 'PUT'})
458 req.remote_user = 'act:usr,act,AUTH_old'304 req.remote_user = 'act:usr,act,AUTH_old'
459 resp = self.test_auth.authorize(req)305 resp = self.test_auth.authorize(req)
460 self.assertEquals(resp.status_int, 403)306 self.assertEquals(resp.status_int, 403)
461307
462 req = Request.blank('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'})308 req = self._make_request('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'})
463 req.remote_user = 'act:usr,act,.reseller_admin'309 req.remote_user = 'act:usr,act,.reseller_admin'
464 resp = self.test_auth.authorize(req)310 resp = self.test_auth.authorize(req)
465 self.assertEquals(resp, None)311 self.assertEquals(resp, None)
466312
467 # .super_admin is not something the middleware should ever see or care313 # .super_admin is not something the middleware should ever see or care
468 # about314 # about
469 req = Request.blank('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'})315 req = self._make_request('/v1/AUTH_new', environ={'REQUEST_METHOD': 'PUT'})
470 req.remote_user = 'act:usr,act,.super_admin'316 req.remote_user = 'act:usr,act,.super_admin'
471 resp = self.test_auth.authorize(req)317 resp = self.test_auth.authorize(req)
472 self.assertEquals(resp.status_int, 403)318 self.assertEquals(resp.status_int, 403)
473319
474 def test_account_delete_permissions(self):320 def test_account_delete_permissions(self):
475 req = Request.blank('/v1/AUTH_new',321 req = self._make_request('/v1/AUTH_new',
476 environ={'REQUEST_METHOD': 'DELETE'})322 environ={'REQUEST_METHOD': 'DELETE'})
477 req.remote_user = 'act:usr,act'323 req.remote_user = 'act:usr,act'
478 resp = self.test_auth.authorize(req)324 resp = self.test_auth.authorize(req)
479 self.assertEquals(resp.status_int, 403)325 self.assertEquals(resp.status_int, 403)
480326
481 req = Request.blank('/v1/AUTH_new',327 req = self._make_request('/v1/AUTH_new',
482 environ={'REQUEST_METHOD': 'DELETE'})328 environ={'REQUEST_METHOD': 'DELETE'})
483 req.remote_user = 'act:usr,act,AUTH_other'329 req.remote_user = 'act:usr,act,AUTH_other'
484 resp = self.test_auth.authorize(req)330 resp = self.test_auth.authorize(req)
485 self.assertEquals(resp.status_int, 403)331 self.assertEquals(resp.status_int, 403)
486332
487 # Even DELETEs to your own account as account admin should fail333 # Even DELETEs to your own account as account admin should fail
488 req = Request.blank('/v1/AUTH_old',334 req = self._make_request('/v1/AUTH_old',
489 environ={'REQUEST_METHOD': 'DELETE'})335 environ={'REQUEST_METHOD': 'DELETE'})
490 req.remote_user = 'act:usr,act,AUTH_old'336 req.remote_user = 'act:usr,act,AUTH_old'
491 resp = self.test_auth.authorize(req)337 resp = self.test_auth.authorize(req)
492 self.assertEquals(resp.status_int, 403)338 self.assertEquals(resp.status_int, 403)
493339
494 req = Request.blank('/v1/AUTH_new',340 req = self._make_request('/v1/AUTH_new',
495 environ={'REQUEST_METHOD': 'DELETE'})341 environ={'REQUEST_METHOD': 'DELETE'})
496 req.remote_user = 'act:usr,act,.reseller_admin'342 req.remote_user = 'act:usr,act,.reseller_admin'
497 resp = self.test_auth.authorize(req)343 resp = self.test_auth.authorize(req)
@@ -499,7 +345,7 @@
499345
500 # .super_admin is not something the middleware should ever see or care346 # .super_admin is not something the middleware should ever see or care
501 # about347 # about
502 req = Request.blank('/v1/AUTH_new',348 req = self._make_request('/v1/AUTH_new',
503 environ={'REQUEST_METHOD': 'DELETE'})349 environ={'REQUEST_METHOD': 'DELETE'})
504 req.remote_user = 'act:usr,act,.super_admin'350 req.remote_user = 'act:usr,act,.super_admin'
505 resp = self.test_auth.authorize(req)351 resp = self.test_auth.authorize(req)
@@ -507,2715 +353,36 @@
507 self.assertEquals(resp.status_int, 403)353 self.assertEquals(resp.status_int, 403)
508354
509 def test_get_token_fail(self):355 def test_get_token_fail(self):
510 resp = Request.blank('/auth/v1.0').get_response(self.test_auth)356 resp = self._make_request('/auth/v1.0').get_response(self.test_auth)
511 self.assertEquals(resp.status_int, 401)357 self.assertEquals(resp.status_int, 401)
512 resp = Request.blank('/auth/v1.0',358 resp = self._make_request('/auth/v1.0',
513 headers={'X-Auth-User': 'act:usr',359 headers={'X-Auth-User': 'act:usr',
514 'X-Auth-Key': 'key'}).get_response(self.test_auth)360 'X-Auth-Key': 'key'}).get_response(self.test_auth)
515 self.assertEquals(resp.status_int, 401)361 self.assertEquals(resp.status_int, 401)
516362
517 def test_get_token_fail_invalid_key(self):
518 self.test_auth.app = FakeApp(iter([
519 # GET of user object
520 ('200 Ok', {},
521 json.dumps({"auth": "plaintext:key",
522 "groups": [{'name': "act:usr"}, {'name': "act"},
523 {'name': ".admin"}]}))]))
524 resp = Request.blank('/auth/v1.0',
525 headers={'X-Auth-User': 'act:usr',
526 'X-Auth-Key': 'invalid'}).get_response(self.test_auth)
527 self.assertEquals(resp.status_int, 401)
528 self.assertEquals(self.test_auth.app.calls, 1)
529
530 def test_get_token_fail_invalid_x_auth_user_format(self):363 def test_get_token_fail_invalid_x_auth_user_format(self):
531 resp = Request.blank('/auth/v1/act/auth',364 resp = self._make_request('/auth/v1/act/auth',
532 headers={'X-Auth-User': 'usr',365 headers={'X-Auth-User': 'usr',
533 'X-Auth-Key': 'key'}).get_response(self.test_auth)366 'X-Auth-Key': 'key'}).get_response(self.test_auth)
534 self.assertEquals(resp.status_int, 401)367 self.assertEquals(resp.status_int, 401)
535368
536 def test_get_token_fail_non_matching_account_in_request(self):369 def test_get_token_fail_non_matching_account_in_request(self):
537 resp = Request.blank('/auth/v1/act/auth',370 resp = self._make_request('/auth/v1/act/auth',
538 headers={'X-Auth-User': 'act2:usr',371 headers={'X-Auth-User': 'act2:usr',
539 'X-Auth-Key': 'key'}).get_response(self.test_auth)372 'X-Auth-Key': 'key'}).get_response(self.test_auth)
540 self.assertEquals(resp.status_int, 401)373 self.assertEquals(resp.status_int, 401)
541374
542 def test_get_token_fail_bad_path(self):375 def test_get_token_fail_bad_path(self):
543 resp = Request.blank('/auth/v1/act/auth/invalid',376 resp = self._make_request('/auth/v1/act/auth/invalid',
544 headers={'X-Auth-User': 'act:usr',377 headers={'X-Auth-User': 'act:usr',
545 'X-Auth-Key': 'key'}).get_response(self.test_auth)378 'X-Auth-Key': 'key'}).get_response(self.test_auth)
546 self.assertEquals(resp.status_int, 400)379 self.assertEquals(resp.status_int, 400)
547380
548 def test_get_token_fail_missing_key(self):381 def test_get_token_fail_missing_key(self):
549 resp = Request.blank('/auth/v1/act/auth',382 resp = self._make_request('/auth/v1/act/auth',
550 headers={'X-Auth-User': 'act:usr'}).get_response(self.test_auth)383 headers={'X-Auth-User': 'act:usr'}).get_response(self.test_auth)
551 self.assertEquals(resp.status_int, 401)384 self.assertEquals(resp.status_int, 401)
552385
553 def test_get_token_fail_get_user_details(self):
554 self.test_auth.app = FakeApp(iter([
555 ('503 Service Unavailable', {}, '')]))
556 resp = Request.blank('/auth/v1.0',
557 headers={'X-Auth-User': 'act:usr',
558 'X-Auth-Key': 'key'}).get_response(self.test_auth)
559 self.assertEquals(resp.status_int, 500)
560 self.assertEquals(self.test_auth.app.calls, 1)
561
562 def test_get_token_fail_get_account(self):
563 self.test_auth.app = FakeApp(iter([
564 # GET of user object
565 ('200 Ok', {},
566 json.dumps({"auth": "plaintext:key",
567 "groups": [{'name': "act:usr"}, {'name': "act"},
568 {'name': ".admin"}]})),
569 # GET of account
570 ('503 Service Unavailable', {}, '')]))
571 resp = Request.blank('/auth/v1.0',
572 headers={'X-Auth-User': 'act:usr',
573 'X-Auth-Key': 'key'}).get_response(self.test_auth)
574 self.assertEquals(resp.status_int, 500)
575 self.assertEquals(self.test_auth.app.calls, 2)
576
577 def test_get_token_fail_put_new_token(self):
578 self.test_auth.app = FakeApp(iter([
579 # GET of user object
580 ('200 Ok', {},
581 json.dumps({"auth": "plaintext:key",
582 "groups": [{'name': "act:usr"}, {'name': "act"},
583 {'name': ".admin"}]})),
584 # GET of account
585 ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
586 # PUT of new token
587 ('503 Service Unavailable', {}, '')]))
588 resp = Request.blank('/auth/v1.0',
589 headers={'X-Auth-User': 'act:usr',
590 'X-Auth-Key': 'key'}).get_response(self.test_auth)
591 self.assertEquals(resp.status_int, 500)
592 self.assertEquals(self.test_auth.app.calls, 3)
593
594 def test_get_token_fail_post_to_user(self):
595 self.test_auth.app = FakeApp(iter([
596 # GET of user object
597 ('200 Ok', {},
598 json.dumps({"auth": "plaintext:key",
599 "groups": [{'name': "act:usr"}, {'name': "act"},
600 {'name': ".admin"}]})),
601 # GET of account
602 ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
603 # PUT of new token
604 ('201 Created', {}, ''),
605 # POST of token to user object
606 ('503 Service Unavailable', {}, '')]))
607 resp = Request.blank('/auth/v1.0',
608 headers={'X-Auth-User': 'act:usr',
609 'X-Auth-Key': 'key'}).get_response(self.test_auth)
610 self.assertEquals(resp.status_int, 500)
611 self.assertEquals(self.test_auth.app.calls, 4)
612
613 def test_get_token_fail_get_services(self):
614 self.test_auth.app = FakeApp(iter([
615 # GET of user object
616 ('200 Ok', {},
617 json.dumps({"auth": "plaintext:key",
618 "groups": [{'name': "act:usr"}, {'name': "act"},
619 {'name': ".admin"}]})),
620 # GET of account
621 ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
622 # PUT of new token
623 ('201 Created', {}, ''),
624 # POST of token to user object
625 ('204 No Content', {}, ''),
626 # GET of services object
627 ('503 Service Unavailable', {}, '')]))
628 resp = Request.blank('/auth/v1.0',
629 headers={'X-Auth-User': 'act:usr',
630 'X-Auth-Key': 'key'}).get_response(self.test_auth)
631 self.assertEquals(resp.status_int, 500)
632 self.assertEquals(self.test_auth.app.calls, 5)
633
634 def test_get_token_fail_get_existing_token(self):
635 self.test_auth.app = FakeApp(iter([
636 # GET of user object
637 ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tktest'},
638 json.dumps({"auth": "plaintext:key",
639 "groups": [{'name': "act:usr"}, {'name': "act"},
640 {'name': ".admin"}]})),
641 # GET of token
642 ('503 Service Unavailable', {}, '')]))
643 resp = Request.blank('/auth/v1.0',
644 headers={'X-Auth-User': 'act:usr',
645 'X-Auth-Key': 'key'}).get_response(self.test_auth)
646 self.assertEquals(resp.status_int, 500)
647 self.assertEquals(self.test_auth.app.calls, 2)
648
649 def test_get_token_success_v1_0(self):
650 self.test_auth.app = FakeApp(iter([
651 # GET of user object
652 ('200 Ok', {},
653 json.dumps({"auth": "plaintext:key",
654 "groups": [{'name': "act:usr"}, {'name': "act"},
655 {'name': ".admin"}]})),
656 # GET of account
657 ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
658 # PUT of new token
659 ('201 Created', {}, ''),
660 # POST of token to user object
661 ('204 No Content', {}, ''),
662 # GET of services object
663 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
664 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
665 resp = Request.blank('/auth/v1.0',
666 headers={'X-Auth-User': 'act:usr',
667 'X-Auth-Key': 'key'}).get_response(self.test_auth)
668 self.assertEquals(resp.status_int, 200)
669 self.assert_(resp.headers.get('x-auth-token',
670 '').startswith('AUTH_tk'), resp.headers.get('x-auth-token'))
671 self.assertEquals(resp.headers.get('x-auth-token'),
672 resp.headers.get('x-storage-token'))
673 self.assertEquals(resp.headers.get('x-storage-url'),
674 'http://127.0.0.1:8080/v1/AUTH_cfa')
675 self.assertEquals(json.loads(resp.body),
676 {"storage": {"default": "local",
677 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
678 self.assertEquals(self.test_auth.app.calls, 5)
679
680 def test_get_token_success_v1_act_auth(self):
681 self.test_auth.app = FakeApp(iter([
682 # GET of user object
683 ('200 Ok', {},
684 json.dumps({"auth": "plaintext:key",
685 "groups": [{'name': "act:usr"}, {'name': "act"},
686 {'name': ".admin"}]})),
687 # GET of account
688 ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
689 # PUT of new token
690 ('201 Created', {}, ''),
691 # POST of token to user object
692 ('204 No Content', {}, ''),
693 # GET of services object
694 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
695 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
696 resp = Request.blank('/auth/v1/act/auth',
697 headers={'X-Storage-User': 'usr',
698 'X-Storage-Pass': 'key'}).get_response(self.test_auth)
699 self.assertEquals(resp.status_int, 200)
700 self.assert_(resp.headers.get('x-auth-token',
701 '').startswith('AUTH_tk'), resp.headers.get('x-auth-token'))
702 self.assertEquals(resp.headers.get('x-auth-token'),
703 resp.headers.get('x-storage-token'))
704 self.assertEquals(resp.headers.get('x-storage-url'),
705 'http://127.0.0.1:8080/v1/AUTH_cfa')
706 self.assertEquals(json.loads(resp.body),
707 {"storage": {"default": "local",
708 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
709 self.assertEquals(self.test_auth.app.calls, 5)
710
711 def test_get_token_success_storage_instead_of_auth(self):
712 self.test_auth.app = FakeApp(iter([
713 # GET of user object
714 ('200 Ok', {},
715 json.dumps({"auth": "plaintext:key",
716 "groups": [{'name': "act:usr"}, {'name': "act"},
717 {'name': ".admin"}]})),
718 # GET of account
719 ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
720 # PUT of new token
721 ('201 Created', {}, ''),
722 # POST of token to user object
723 ('204 No Content', {}, ''),
724 # GET of services object
725 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
726 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
727 resp = Request.blank('/auth/v1.0',
728 headers={'X-Storage-User': 'act:usr',
729 'X-Storage-Pass': 'key'}).get_response(self.test_auth)
730 self.assertEquals(resp.status_int, 200)
731 self.assert_(resp.headers.get('x-auth-token',
732 '').startswith('AUTH_tk'), resp.headers.get('x-auth-token'))
733 self.assertEquals(resp.headers.get('x-auth-token'),
734 resp.headers.get('x-storage-token'))
735 self.assertEquals(resp.headers.get('x-storage-url'),
736 'http://127.0.0.1:8080/v1/AUTH_cfa')
737 self.assertEquals(json.loads(resp.body),
738 {"storage": {"default": "local",
739 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
740 self.assertEquals(self.test_auth.app.calls, 5)
741
742 def test_get_token_success_v1_act_auth_auth_instead_of_storage(self):
743 self.test_auth.app = FakeApp(iter([
744 # GET of user object
745 ('200 Ok', {},
746 json.dumps({"auth": "plaintext:key",
747 "groups": [{'name': "act:usr"}, {'name': "act"},
748 {'name': ".admin"}]})),
749 # GET of account
750 ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
751 # PUT of new token
752 ('201 Created', {}, ''),
753 # POST of token to user object
754 ('204 No Content', {}, ''),
755 # GET of services object
756 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
757 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
758 resp = Request.blank('/auth/v1/act/auth',
759 headers={'X-Auth-User': 'act:usr',
760 'X-Auth-Key': 'key'}).get_response(self.test_auth)
761 self.assertEquals(resp.status_int, 200)
762 self.assert_(resp.headers.get('x-auth-token',
763 '').startswith('AUTH_tk'), resp.headers.get('x-auth-token'))
764 self.assertEquals(resp.headers.get('x-auth-token'),
765 resp.headers.get('x-storage-token'))
766 self.assertEquals(resp.headers.get('x-storage-url'),
767 'http://127.0.0.1:8080/v1/AUTH_cfa')
768 self.assertEquals(json.loads(resp.body),
769 {"storage": {"default": "local",
770 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
771 self.assertEquals(self.test_auth.app.calls, 5)
772
773 def test_get_token_success_existing_token(self):
774 self.test_auth.app = FakeApp(iter([
775 # GET of user object
776 ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tktest'},
777 json.dumps({"auth": "plaintext:key",
778 "groups": [{'name': "act:usr"}, {'name': "act"},
779 {'name': ".admin"}]})),
780 # GET of token
781 ('200 Ok', {}, json.dumps({"account": "act", "user": "usr",
782 "account_id": "AUTH_cfa", "groups": [{'name': "act:usr"},
783 {'name': "key"}, {'name': ".admin"}],
784 "expires": 9999999999.9999999})),
785 # GET of services object
786 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
787 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
788 resp = Request.blank('/auth/v1.0',
789 headers={'X-Auth-User': 'act:usr',
790 'X-Auth-Key': 'key'}).get_response(self.test_auth)
791 self.assertEquals(resp.status_int, 200)
792 self.assertEquals(resp.headers.get('x-auth-token'), 'AUTH_tktest')
793 self.assertEquals(resp.headers.get('x-auth-token'),
794 resp.headers.get('x-storage-token'))
795 self.assertEquals(resp.headers.get('x-storage-url'),
796 'http://127.0.0.1:8080/v1/AUTH_cfa')
797 self.assertEquals(json.loads(resp.body),
798 {"storage": {"default": "local",
799 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
800 self.assertEquals(self.test_auth.app.calls, 3)
801
802 def test_get_token_success_existing_token_expired(self):
803 self.test_auth.app = FakeApp(iter([
804 # GET of user object
805 ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tktest'},
806 json.dumps({"auth": "plaintext:key",
807 "groups": [{'name': "act:usr"}, {'name': "act"},
808 {'name': ".admin"}]})),
809 # GET of token
810 ('200 Ok', {}, json.dumps({"account": "act", "user": "usr",
811 "account_id": "AUTH_cfa", "groups": [{'name': "act:usr"},
812 {'name': "key"}, {'name': ".admin"}],
813 "expires": 0.0})),
814 # DELETE of expired token
815 ('204 No Content', {}, ''),
816 # GET of account
817 ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
818 # PUT of new token
819 ('201 Created', {}, ''),
820 # POST of token to user object
821 ('204 No Content', {}, ''),
822 # GET of services object
823 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
824 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
825 resp = Request.blank('/auth/v1.0',
826 headers={'X-Auth-User': 'act:usr',
827 'X-Auth-Key': 'key'}).get_response(self.test_auth)
828 self.assertEquals(resp.status_int, 200)
829 self.assertNotEquals(resp.headers.get('x-auth-token'), 'AUTH_tktest')
830 self.assertEquals(resp.headers.get('x-auth-token'),
831 resp.headers.get('x-storage-token'))
832 self.assertEquals(resp.headers.get('x-storage-url'),
833 'http://127.0.0.1:8080/v1/AUTH_cfa')
834 self.assertEquals(json.loads(resp.body),
835 {"storage": {"default": "local",
836 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
837 self.assertEquals(self.test_auth.app.calls, 7)
838
839 def test_get_token_success_existing_token_expired_fail_deleting_old(self):
840 self.test_auth.app = FakeApp(iter([
841 # GET of user object
842 ('200 Ok', {'X-Object-Meta-Auth-Token': 'AUTH_tktest'},
843 json.dumps({"auth": "plaintext:key",
844 "groups": [{'name': "act:usr"}, {'name': "act"},
845 {'name': ".admin"}]})),
846 # GET of token
847 ('200 Ok', {}, json.dumps({"account": "act", "user": "usr",
848 "account_id": "AUTH_cfa", "groups": [{'name': "act:usr"},
849 {'name': "key"}, {'name': ".admin"}],
850 "expires": 0.0})),
851 # DELETE of expired token
852 ('503 Service Unavailable', {}, ''),
853 # GET of account
854 ('204 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''),
855 # PUT of new token
856 ('201 Created', {}, ''),
857 # POST of token to user object
858 ('204 No Content', {}, ''),
859 # GET of services object
860 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
861 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}}))]))
862 resp = Request.blank('/auth/v1.0',
863 headers={'X-Auth-User': 'act:usr',
864 'X-Auth-Key': 'key'}).get_response(self.test_auth)
865 self.assertEquals(resp.status_int, 200)
866 self.assertNotEquals(resp.headers.get('x-auth-token'), 'AUTH_tktest')
867 self.assertEquals(resp.headers.get('x-auth-token'),
868 resp.headers.get('x-storage-token'))
869 self.assertEquals(resp.headers.get('x-storage-url'),
870 'http://127.0.0.1:8080/v1/AUTH_cfa')
871 self.assertEquals(json.loads(resp.body),
872 {"storage": {"default": "local",
873 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})
874 self.assertEquals(self.test_auth.app.calls, 7)
875
876 def test_prep_success(self):
877 list_to_iter = [
878 # PUT of .auth account
879 ('201 Created', {}, ''),
880 # PUT of .account_id container
881 ('201 Created', {}, '')]
882 # PUT of .token* containers
883 for x in xrange(16):
884 list_to_iter.append(('201 Created', {}, ''))
885 self.test_auth.app = FakeApp(iter(list_to_iter))
886 resp = Request.blank('/auth/v2/.prep',
887 environ={'REQUEST_METHOD': 'POST'},
888 headers={'X-Auth-Admin-User': '.super_admin',
889 'X-Auth-Admin-Key': 'supertest'}
890 ).get_response(self.test_auth)
891 self.assertEquals(resp.status_int, 204)
892 self.assertEquals(self.test_auth.app.calls, 18)
893
894 def test_prep_bad_method(self):
895 resp = Request.blank('/auth/v2/.prep',
896 headers={'X-Auth-Admin-User': '.super_admin',
897 'X-Auth-Admin-Key': 'supertest'}
898 ).get_response(self.test_auth)
899 self.assertEquals(resp.status_int, 400)
900 resp = Request.blank('/auth/v2/.prep',
901 environ={'REQUEST_METHOD': 'HEAD'},
902 headers={'X-Auth-Admin-User': '.super_admin',
903 'X-Auth-Admin-Key': 'supertest'}
904 ).get_response(self.test_auth)
905 self.assertEquals(resp.status_int, 400)
906 resp = Request.blank('/auth/v2/.prep',
907 environ={'REQUEST_METHOD': 'PUT'},
908 headers={'X-Auth-Admin-User': '.super_admin',
909 'X-Auth-Admin-Key': 'supertest'}
910 ).get_response(self.test_auth)
911 self.assertEquals(resp.status_int, 400)
912
913 def test_prep_bad_creds(self):
914 resp = Request.blank('/auth/v2/.prep',
915 environ={'REQUEST_METHOD': 'POST'},
916 headers={'X-Auth-Admin-User': 'super_admin',
917 'X-Auth-Admin-Key': 'supertest'}
918 ).get_response(self.test_auth)
919 self.assertEquals(resp.status_int, 403)
920 resp = Request.blank('/auth/v2/.prep',
921 environ={'REQUEST_METHOD': 'POST'},
922 headers={'X-Auth-Admin-User': '.super_admin',
923 'X-Auth-Admin-Key': 'upertest'}
924 ).get_response(self.test_auth)
925 self.assertEquals(resp.status_int, 403)
926 resp = Request.blank('/auth/v2/.prep',
927 environ={'REQUEST_METHOD': 'POST'},
928 headers={'X-Auth-Admin-User': '.super_admin'}
929 ).get_response(self.test_auth)
930 self.assertEquals(resp.status_int, 403)
931 resp = Request.blank('/auth/v2/.prep',
932 environ={'REQUEST_METHOD': 'POST'},
933 headers={'X-Auth-Admin-Key': 'supertest'}
934 ).get_response(self.test_auth)
935 self.assertEquals(resp.status_int, 403)
936 resp = Request.blank('/auth/v2/.prep',
937 environ={'REQUEST_METHOD': 'POST'}).get_response(self.test_auth)
938 self.assertEquals(resp.status_int, 403)
939
940 def test_prep_fail_account_create(self):
941 self.test_auth.app = FakeApp(iter([
942 # PUT of .auth account
943 ('503 Service Unavailable', {}, '')]))
944 resp = Request.blank('/auth/v2/.prep',
945 environ={'REQUEST_METHOD': 'POST'},
946 headers={'X-Auth-Admin-User': '.super_admin',
947 'X-Auth-Admin-Key': 'supertest'}
948 ).get_response(self.test_auth)
949 self.assertEquals(resp.status_int, 500)
950 self.assertEquals(self.test_auth.app.calls, 1)
951
952 def test_prep_fail_token_container_create(self):
953 self.test_auth.app = FakeApp(iter([
954 # PUT of .auth account
955 ('201 Created', {}, ''),
956 # PUT of .token container
957 ('503 Service Unavailable', {}, '')]))
958 resp = Request.blank('/auth/v2/.prep',
959 environ={'REQUEST_METHOD': 'POST'},
960 headers={'X-Auth-Admin-User': '.super_admin',
961 'X-Auth-Admin-Key': 'supertest'}
962 ).get_response(self.test_auth)
963 self.assertEquals(resp.status_int, 500)
964 self.assertEquals(self.test_auth.app.calls, 2)
965
966 def test_prep_fail_account_id_container_create(self):
967 self.test_auth.app = FakeApp(iter([
968 # PUT of .auth account
969 ('201 Created', {}, ''),
970 # PUT of .token container
971 ('201 Created', {}, ''),
972 # PUT of .account_id container
973 ('503 Service Unavailable', {}, '')]))
974 resp = Request.blank('/auth/v2/.prep',
975 environ={'REQUEST_METHOD': 'POST'},
976 headers={'X-Auth-Admin-User': '.super_admin',
977 'X-Auth-Admin-Key': 'supertest'}
978 ).get_response(self.test_auth)
979 self.assertEquals(resp.status_int, 500)
980 self.assertEquals(self.test_auth.app.calls, 3)
981
982 def test_get_reseller_success(self):
983 self.test_auth.app = FakeApp(iter([
984 # GET of .auth account (list containers)
985 ('200 Ok', {}, json.dumps([
986 {"name": ".token", "count": 0, "bytes": 0},
987 {"name": ".account_id", "count": 0, "bytes": 0},
988 {"name": "act", "count": 0, "bytes": 0}])),
989 # GET of .auth account (list containers continuation)
990 ('200 Ok', {}, '[]')]))
991 resp = Request.blank('/auth/v2',
992 headers={'X-Auth-Admin-User': '.super_admin',
993 'X-Auth-Admin-Key': 'supertest'}
994 ).get_response(self.test_auth)
995 self.assertEquals(resp.status_int, 200)
996 self.assertEquals(json.loads(resp.body),
997 {"accounts": [{"name": "act"}]})
998 self.assertEquals(self.test_auth.app.calls, 2)
999
1000 self.test_auth.app = FakeApp(iter([
1001 # GET of user object
1002 ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"},
1003 {"name": "test"}, {"name": ".admin"},
1004 {"name": ".reseller_admin"}], "auth": "plaintext:key"})),
1005 # GET of .auth account (list containers)
1006 ('200 Ok', {}, json.dumps([
1007 {"name": ".token", "count": 0, "bytes": 0},
1008 {"name": ".account_id", "count": 0, "bytes": 0},
1009 {"name": "act", "count": 0, "bytes": 0}])),
1010 # GET of .auth account (list containers continuation)
1011 ('200 Ok', {}, '[]')]))
1012 resp = Request.blank('/auth/v2',
1013 headers={'X-Auth-Admin-User': 'act:adm',
1014 'X-Auth-Admin-Key': 'key'}
1015 ).get_response(self.test_auth)
1016 self.assertEquals(resp.status_int, 200)
1017 self.assertEquals(json.loads(resp.body),
1018 {"accounts": [{"name": "act"}]})
1019 self.assertEquals(self.test_auth.app.calls, 3)
1020
1021 def test_get_reseller_fail_bad_creds(self):
1022 self.test_auth.app = FakeApp(iter([
1023 # GET of user object
1024 ('404 Not Found', {}, '')]))
1025 resp = Request.blank('/auth/v2',
1026 headers={'X-Auth-Admin-User': 'super:admin',
1027 'X-Auth-Admin-Key': 'supertest'}
1028 ).get_response(self.test_auth)
1029 self.assertEquals(resp.status_int, 403)
1030 self.assertEquals(self.test_auth.app.calls, 1)
1031
1032 self.test_auth.app = FakeApp(iter([
1033 # GET of user object (account admin, but not reseller admin)
1034 ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"},
1035 {"name": "test"}, {"name": ".admin"}],
1036 "auth": "plaintext:key"}))]))
1037 resp = Request.blank('/auth/v2',
1038 headers={'X-Auth-Admin-User': 'act:adm',
1039 'X-Auth-Admin-Key': 'key'}
1040 ).get_response(self.test_auth)
1041 self.assertEquals(resp.status_int, 403)
1042 self.assertEquals(self.test_auth.app.calls, 1)
1043
1044 self.test_auth.app = FakeApp(iter([
1045 # GET of user object (regular user)
1046 ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"},
1047 {"name": "test"}], "auth": "plaintext:key"}))]))
1048 resp = Request.blank('/auth/v2',
1049 headers={'X-Auth-Admin-User': 'act:usr',
1050 'X-Auth-Admin-Key': 'key'}
1051 ).get_response(self.test_auth)
1052 self.assertEquals(resp.status_int, 403)
1053 self.assertEquals(self.test_auth.app.calls, 1)
1054
1055 def test_get_reseller_fail_listing(self):
1056 self.test_auth.app = FakeApp(iter([
1057 # GET of .auth account (list containers)
1058 ('503 Service Unavailable', {}, '')]))
1059 resp = Request.blank('/auth/v2',
1060 headers={'X-Auth-Admin-User': '.super_admin',
1061 'X-Auth-Admin-Key': 'supertest'}
1062 ).get_response(self.test_auth)
1063 self.assertEquals(resp.status_int, 500)
1064 self.assertEquals(self.test_auth.app.calls, 1)
1065
1066 self.test_auth.app = FakeApp(iter([
1067 # GET of .auth account (list containers)
1068 ('200 Ok', {}, json.dumps([
1069 {"name": ".token", "count": 0, "bytes": 0},
1070 {"name": ".account_id", "count": 0, "bytes": 0},
1071 {"name": "act", "count": 0, "bytes": 0}])),
1072 # GET of .auth account (list containers continuation)
1073 ('503 Service Unavailable', {}, '')]))
1074 resp = Request.blank('/auth/v2',
1075 headers={'X-Auth-Admin-User': '.super_admin',
1076 'X-Auth-Admin-Key': 'supertest'}
1077 ).get_response(self.test_auth)
1078 self.assertEquals(resp.status_int, 500)
1079 self.assertEquals(self.test_auth.app.calls, 2)
1080
1081 def test_get_account_success(self):
1082 self.test_auth.app = FakeApp(iter([
1083 # GET of .services object
1084 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
1085 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
1086 # GET of account container (list objects)
1087 ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
1088 json.dumps([
1089 {"name": ".services", "hash": "etag", "bytes": 112,
1090 "content_type": "application/octet-stream",
1091 "last_modified": "2010-12-03T17:16:27.618110"},
1092 {"name": "tester", "hash": "etag", "bytes": 104,
1093 "content_type": "application/octet-stream",
1094 "last_modified": "2010-12-03T17:16:27.736680"},
1095 {"name": "tester3", "hash": "etag", "bytes": 86,
1096 "content_type": "application/octet-stream",
1097 "last_modified": "2010-12-03T17:16:28.135530"}])),
1098 # GET of account container (list objects continuation)
1099 ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]')]))
1100 resp = Request.blank('/auth/v2/act',
1101 headers={'X-Auth-Admin-User': '.super_admin',
1102 'X-Auth-Admin-Key': 'supertest'}
1103 ).get_response(self.test_auth)
1104 self.assertEquals(resp.status_int, 200)
1105 self.assertEquals(json.loads(resp.body),
1106 {'account_id': 'AUTH_cfa',
1107 'services': {'storage':
1108 {'default': 'local',
1109 'local': 'http://127.0.0.1:8080/v1/AUTH_cfa'}},
1110 'users': [{'name': 'tester'}, {'name': 'tester3'}]})
1111 self.assertEquals(self.test_auth.app.calls, 3)
1112
1113 self.test_auth.app = FakeApp(iter([
1114 # GET of user object
1115 ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"},
1116 {"name": "test"}, {"name": ".admin"}],
1117 "auth": "plaintext:key"})),
1118 # GET of .services object
1119 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
1120 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
1121 # GET of account container (list objects)
1122 ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
1123 json.dumps([
1124 {"name": ".services", "hash": "etag", "bytes": 112,
1125 "content_type": "application/octet-stream",
1126 "last_modified": "2010-12-03T17:16:27.618110"},
1127 {"name": "tester", "hash": "etag", "bytes": 104,
1128 "content_type": "application/octet-stream",
1129 "last_modified": "2010-12-03T17:16:27.736680"},
1130 {"name": "tester3", "hash": "etag", "bytes": 86,
1131 "content_type": "application/octet-stream",
1132 "last_modified": "2010-12-03T17:16:28.135530"}])),
1133 # GET of account container (list objects continuation)
1134 ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]')]))
1135 resp = Request.blank('/auth/v2/act',
1136 headers={'X-Auth-Admin-User': 'act:adm',
1137 'X-Auth-Admin-Key': 'key'}
1138 ).get_response(self.test_auth)
1139 self.assertEquals(resp.status_int, 200)
1140 self.assertEquals(json.loads(resp.body),
1141 {'account_id': 'AUTH_cfa',
1142 'services': {'storage':
1143 {'default': 'local',
1144 'local': 'http://127.0.0.1:8080/v1/AUTH_cfa'}},
1145 'users': [{'name': 'tester'}, {'name': 'tester3'}]})
1146 self.assertEquals(self.test_auth.app.calls, 4)
1147
1148 def test_get_account_fail_bad_account_name(self):
1149 resp = Request.blank('/auth/v2/.token',
1150 headers={'X-Auth-Admin-User': '.super_admin',
1151 'X-Auth-Admin-Key': 'supertest'}
1152 ).get_response(self.test_auth)
1153 self.assertEquals(resp.status_int, 400)
1154 resp = Request.blank('/auth/v2/.anything',
1155 headers={'X-Auth-Admin-User': '.super_admin',
1156 'X-Auth-Admin-Key': 'supertest'}
1157 ).get_response(self.test_auth)
1158 self.assertEquals(resp.status_int, 400)
1159
1160 def test_get_account_fail_creds(self):
1161 self.test_auth.app = FakeApp(iter([
1162 # GET of user object
1163 ('404 Not Found', {}, '')]))
1164 resp = Request.blank('/auth/v2/act',
1165 headers={'X-Auth-Admin-User': 'super:admin',
1166 'X-Auth-Admin-Key': 'supertest'}
1167 ).get_response(self.test_auth)
1168 self.assertEquals(resp.status_int, 403)
1169 self.assertEquals(self.test_auth.app.calls, 1)
1170
1171 self.test_auth.app = FakeApp(iter([
1172 # GET of user object (account admin, but wrong account)
1173 ('200 Ok', {}, json.dumps({"groups": [{"name": "act2:adm"},
1174 {"name": "test"}, {"name": ".admin"}],
1175 "auth": "plaintext:key"}))]))
1176 resp = Request.blank('/auth/v2/act',
1177 headers={'X-Auth-Admin-User': 'act2:adm',
1178 'X-Auth-Admin-Key': 'key'}
1179 ).get_response(self.test_auth)
1180 self.assertEquals(resp.status_int, 403)
1181 self.assertEquals(self.test_auth.app.calls, 1)
1182
1183 self.test_auth.app = FakeApp(iter([
1184 # GET of user object (regular user)
1185 ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"},
1186 {"name": "test"}], "auth": "plaintext:key"}))]))
1187 resp = Request.blank('/auth/v2/act',
1188 headers={'X-Auth-Admin-User': 'act:usr',
1189 'X-Auth-Admin-Key': 'key'}
1190 ).get_response(self.test_auth)
1191 self.assertEquals(resp.status_int, 403)
1192 self.assertEquals(self.test_auth.app.calls, 1)
1193
1194 def test_get_account_fail_get_services(self):
1195 self.test_auth.app = FakeApp(iter([
1196 # GET of .services object
1197 ('503 Service Unavailable', {}, '')]))
1198 resp = Request.blank('/auth/v2/act',
1199 headers={'X-Auth-Admin-User': '.super_admin',
1200 'X-Auth-Admin-Key': 'supertest'}
1201 ).get_response(self.test_auth)
1202 self.assertEquals(resp.status_int, 500)
1203 self.assertEquals(self.test_auth.app.calls, 1)
1204
1205 self.test_auth.app = FakeApp(iter([
1206 # GET of .services object
1207 ('404 Not Found', {}, '')]))
1208 resp = Request.blank('/auth/v2/act',
1209 headers={'X-Auth-Admin-User': '.super_admin',
1210 'X-Auth-Admin-Key': 'supertest'}
1211 ).get_response(self.test_auth)
1212 self.assertEquals(resp.status_int, 404)
1213 self.assertEquals(self.test_auth.app.calls, 1)
1214
1215 def test_get_account_fail_listing(self):
1216 self.test_auth.app = FakeApp(iter([
1217 # GET of .services object
1218 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
1219 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
1220 # GET of account container (list objects)
1221 ('503 Service Unavailable', {}, '')]))
1222 resp = Request.blank('/auth/v2/act',
1223 headers={'X-Auth-Admin-User': '.super_admin',
1224 'X-Auth-Admin-Key': 'supertest'}
1225 ).get_response(self.test_auth)
1226 self.assertEquals(resp.status_int, 500)
1227 self.assertEquals(self.test_auth.app.calls, 2)
1228
1229 self.test_auth.app = FakeApp(iter([
1230 # GET of .services object
1231 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
1232 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
1233 # GET of account container (list objects)
1234 ('404 Not Found', {}, '')]))
1235 resp = Request.blank('/auth/v2/act',
1236 headers={'X-Auth-Admin-User': '.super_admin',
1237 'X-Auth-Admin-Key': 'supertest'}
1238 ).get_response(self.test_auth)
1239 self.assertEquals(resp.status_int, 404)
1240 self.assertEquals(self.test_auth.app.calls, 2)
1241
1242 self.test_auth.app = FakeApp(iter([
1243 # GET of .services object
1244 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
1245 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
1246 # GET of account container (list objects)
1247 ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
1248 json.dumps([
1249 {"name": ".services", "hash": "etag", "bytes": 112,
1250 "content_type": "application/octet-stream",
1251 "last_modified": "2010-12-03T17:16:27.618110"},
1252 {"name": "tester", "hash": "etag", "bytes": 104,
1253 "content_type": "application/octet-stream",
1254 "last_modified": "2010-12-03T17:16:27.736680"},
1255 {"name": "tester3", "hash": "etag", "bytes": 86,
1256 "content_type": "application/octet-stream",
1257 "last_modified": "2010-12-03T17:16:28.135530"}])),
1258 # GET of account container (list objects continuation)
1259 ('503 Service Unavailable', {}, '')]))
1260 resp = Request.blank('/auth/v2/act',
1261 headers={'X-Auth-Admin-User': '.super_admin',
1262 'X-Auth-Admin-Key': 'supertest'}
1263 ).get_response(self.test_auth)
1264 self.assertEquals(resp.status_int, 500)
1265 self.assertEquals(self.test_auth.app.calls, 3)
1266
1267 def test_set_services_new_service(self):
1268 self.test_auth.app = FakeApp(iter([
1269 # GET of .services object
1270 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
1271 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
1272 # PUT of new .services object
1273 ('204 No Content', {}, '')]))
1274 resp = Request.blank('/auth/v2/act/.services',
1275 environ={'REQUEST_METHOD': 'POST'},
1276 headers={'X-Auth-Admin-User': '.super_admin',
1277 'X-Auth-Admin-Key': 'supertest'},
1278 body=json.dumps({'new_service': {'new_endpoint': 'new_value'}})
1279 ).get_response(self.test_auth)
1280 self.assertEquals(resp.status_int, 200)
1281 self.assertEquals(json.loads(resp.body),
1282 {'storage': {'default': 'local',
1283 'local': 'http://127.0.0.1:8080/v1/AUTH_cfa'},
1284 'new_service': {'new_endpoint': 'new_value'}})
1285 self.assertEquals(self.test_auth.app.calls, 2)
1286
1287 def test_set_services_new_endpoint(self):
1288 self.test_auth.app = FakeApp(iter([
1289 # GET of .services object
1290 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
1291 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
1292 # PUT of new .services object
1293 ('204 No Content', {}, '')]))
1294 resp = Request.blank('/auth/v2/act/.services',
1295 environ={'REQUEST_METHOD': 'POST'},
1296 headers={'X-Auth-Admin-User': '.super_admin',
1297 'X-Auth-Admin-Key': 'supertest'},
1298 body=json.dumps({'storage': {'new_endpoint': 'new_value'}})
1299 ).get_response(self.test_auth)
1300 self.assertEquals(resp.status_int, 200)
1301 self.assertEquals(json.loads(resp.body),
1302 {'storage': {'default': 'local',
1303 'local': 'http://127.0.0.1:8080/v1/AUTH_cfa',
1304 'new_endpoint': 'new_value'}})
1305 self.assertEquals(self.test_auth.app.calls, 2)
1306
1307 def test_set_services_update_endpoint(self):
1308 self.test_auth.app = FakeApp(iter([
1309 # GET of .services object
1310 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
1311 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
1312 # PUT of new .services object
1313 ('204 No Content', {}, '')]))
1314 resp = Request.blank('/auth/v2/act/.services',
1315 environ={'REQUEST_METHOD': 'POST'},
1316 headers={'X-Auth-Admin-User': '.super_admin',
1317 'X-Auth-Admin-Key': 'supertest'},
1318 body=json.dumps({'storage': {'local': 'new_value'}})
1319 ).get_response(self.test_auth)
1320 self.assertEquals(resp.status_int, 200)
1321 self.assertEquals(json.loads(resp.body),
1322 {'storage': {'default': 'local',
1323 'local': 'new_value'}})
1324 self.assertEquals(self.test_auth.app.calls, 2)
1325
1326 def test_set_services_fail_bad_creds(self):
1327 self.test_auth.app = FakeApp(iter([
1328 # GET of user object
1329 ('404 Not Found', {}, '')]))
1330 resp = Request.blank('/auth/v2/act/.services',
1331 environ={'REQUEST_METHOD': 'POST'},
1332 headers={'X-Auth-Admin-User': 'super:admin',
1333 'X-Auth-Admin-Key': 'supertest'},
1334 body=json.dumps({'storage': {'local': 'new_value'}})
1335 ).get_response(self.test_auth)
1336 self.assertEquals(resp.status_int, 403)
1337 self.assertEquals(self.test_auth.app.calls, 1)
1338
1339 self.test_auth.app = FakeApp(iter([
1340 # GET of user object (account admin, but not reseller admin)
1341 ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"},
1342 {"name": "test"}, {"name": ".admin"}],
1343 "auth": "plaintext:key"}))]))
1344 resp = Request.blank('/auth/v2/act/.services',
1345 environ={'REQUEST_METHOD': 'POST'},
1346 headers={'X-Auth-Admin-User': 'act:adm',
1347 'X-Auth-Admin-Key': 'key'},
1348 body=json.dumps({'storage': {'local': 'new_value'}})
1349 ).get_response(self.test_auth)
1350 self.assertEquals(resp.status_int, 403)
1351 self.assertEquals(self.test_auth.app.calls, 1)
1352
1353 self.test_auth.app = FakeApp(iter([
1354 # GET of user object (regular user)
1355 ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"},
1356 {"name": "test"}], "auth": "plaintext:key"}))]))
1357 resp = Request.blank('/auth/v2/act/.services',
1358 environ={'REQUEST_METHOD': 'POST'},
1359 headers={'X-Auth-Admin-User': 'act:usr',
1360 'X-Auth-Admin-Key': 'key'},
1361 body=json.dumps({'storage': {'local': 'new_value'}})
1362 ).get_response(self.test_auth)
1363 self.assertEquals(resp.status_int, 403)
1364 self.assertEquals(self.test_auth.app.calls, 1)
1365
1366 def test_set_services_fail_bad_account_name(self):
1367 resp = Request.blank('/auth/v2/.act/.services',
1368 environ={'REQUEST_METHOD': 'POST'},
1369 headers={'X-Auth-Admin-User': '.super_admin',
1370 'X-Auth-Admin-Key': 'supertest'},
1371 body=json.dumps({'storage': {'local': 'new_value'}})
1372 ).get_response(self.test_auth)
1373 self.assertEquals(resp.status_int, 400)
1374
1375 def test_set_services_fail_bad_json(self):
1376 resp = Request.blank('/auth/v2/act/.services',
1377 environ={'REQUEST_METHOD': 'POST'},
1378 headers={'X-Auth-Admin-User': '.super_admin',
1379 'X-Auth-Admin-Key': 'supertest'},
1380 body='garbage'
1381 ).get_response(self.test_auth)
1382 self.assertEquals(resp.status_int, 400)
1383 resp = Request.blank('/auth/v2/act/.services',
1384 environ={'REQUEST_METHOD': 'POST'},
1385 headers={'X-Auth-Admin-User': '.super_admin',
1386 'X-Auth-Admin-Key': 'supertest'},
1387 body=''
1388 ).get_response(self.test_auth)
1389 self.assertEquals(resp.status_int, 400)
1390
1391 def test_set_services_fail_get_services(self):
1392 self.test_auth.app = FakeApp(iter([
1393 # GET of .services object
1394 ('503 Unavailable', {}, '')]))
1395 resp = Request.blank('/auth/v2/act/.services',
1396 environ={'REQUEST_METHOD': 'POST'},
1397 headers={'X-Auth-Admin-User': '.super_admin',
1398 'X-Auth-Admin-Key': 'supertest'},
1399 body=json.dumps({'new_service': {'new_endpoint': 'new_value'}})
1400 ).get_response(self.test_auth)
1401 self.assertEquals(resp.status_int, 500)
1402 self.assertEquals(self.test_auth.app.calls, 1)
1403
1404 self.test_auth.app = FakeApp(iter([
1405 # GET of .services object
1406 ('404 Not Found', {}, '')]))
1407 resp = Request.blank('/auth/v2/act/.services',
1408 environ={'REQUEST_METHOD': 'POST'},
1409 headers={'X-Auth-Admin-User': '.super_admin',
1410 'X-Auth-Admin-Key': 'supertest'},
1411 body=json.dumps({'new_service': {'new_endpoint': 'new_value'}})
1412 ).get_response(self.test_auth)
1413 self.assertEquals(resp.status_int, 404)
1414 self.assertEquals(self.test_auth.app.calls, 1)
1415
1416 def test_set_services_fail_put_services(self):
1417 self.test_auth.app = FakeApp(iter([
1418 # GET of .services object
1419 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
1420 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
1421 # PUT of new .services object
1422 ('503 Unavailable', {}, '')]))
1423 resp = Request.blank('/auth/v2/act/.services',
1424 environ={'REQUEST_METHOD': 'POST'},
1425 headers={'X-Auth-Admin-User': '.super_admin',
1426 'X-Auth-Admin-Key': 'supertest'},
1427 body=json.dumps({'new_service': {'new_endpoint': 'new_value'}})
1428 ).get_response(self.test_auth)
1429 self.assertEquals(resp.status_int, 500)
1430 self.assertEquals(self.test_auth.app.calls, 2)
1431
1432 def test_put_account_success(self):
1433 conn = FakeConn(iter([
1434 # PUT of storage account itself
1435 ('201 Created', {}, '')]))
1436 self.test_auth.get_conn = lambda: conn
1437 self.test_auth.app = FakeApp(iter([
1438 # Initial HEAD of account container to check for pre-existence
1439 ('404 Not Found', {}, ''),
1440 # PUT of account container
1441 ('204 No Content', {}, ''),
1442 # PUT of .account_id mapping object
1443 ('204 No Content', {}, ''),
1444 # PUT of .services object
1445 ('204 No Content', {}, ''),
1446 # POST to account container updating X-Container-Meta-Account-Id
1447 ('204 No Content', {}, '')]))
1448 resp = Request.blank('/auth/v2/act',
1449 environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
1450 headers={'X-Auth-Admin-User': '.super_admin',
1451 'X-Auth-Admin-Key': 'supertest'}
1452 ).get_response(self.test_auth)
1453 self.assertEquals(resp.status_int, 201)
1454 self.assertEquals(self.test_auth.app.calls, 5)
1455 self.assertEquals(conn.calls, 1)
1456
1457 def test_put_account_success_preexist_but_not_completed(self):
1458 conn = FakeConn(iter([
1459 # PUT of storage account itself
1460 ('201 Created', {}, '')]))
1461 self.test_auth.get_conn = lambda: conn
1462 self.test_auth.app = FakeApp(iter([
1463 # Initial HEAD of account container to check for pre-existence
1464 # We're going to show it as existing this time, but with no
1465 # X-Container-Meta-Account-Id, indicating a failed previous attempt
1466 ('200 Ok', {}, ''),
1467 # PUT of .account_id mapping object
1468 ('204 No Content', {}, ''),
1469 # PUT of .services object
1470 ('204 No Content', {}, ''),
1471 # POST to account container updating X-Container-Meta-Account-Id
1472 ('204 No Content', {}, '')]))
1473 resp = Request.blank('/auth/v2/act',
1474 environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
1475 headers={'X-Auth-Admin-User': '.super_admin',
1476 'X-Auth-Admin-Key': 'supertest'}
1477 ).get_response(self.test_auth)
1478 self.assertEquals(resp.status_int, 201)
1479 self.assertEquals(self.test_auth.app.calls, 4)
1480 self.assertEquals(conn.calls, 1)
1481
1482 def test_put_account_success_preexist_and_completed(self):
1483 self.test_auth.app = FakeApp(iter([
1484 # Initial HEAD of account container to check for pre-existence
1485 # We're going to show it as existing this time, and with an
1486 # X-Container-Meta-Account-Id, indicating it already exists
1487 ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '')]))
1488 resp = Request.blank('/auth/v2/act',
1489 environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
1490 headers={'X-Auth-Admin-User': '.super_admin',
1491 'X-Auth-Admin-Key': 'supertest'}
1492 ).get_response(self.test_auth)
1493 self.assertEquals(resp.status_int, 202)
1494 self.assertEquals(self.test_auth.app.calls, 1)
1495
1496 def test_put_account_success_with_given_suffix(self):
1497 conn = FakeConn(iter([
1498 # PUT of storage account itself
1499 ('201 Created', {}, '')]))
1500 self.test_auth.get_conn = lambda: conn
1501 self.test_auth.app = FakeApp(iter([
1502 # Initial HEAD of account container to check for pre-existence
1503 ('404 Not Found', {}, ''),
1504 # PUT of account container
1505 ('204 No Content', {}, ''),
1506 # PUT of .account_id mapping object
1507 ('204 No Content', {}, ''),
1508 # PUT of .services object
1509 ('204 No Content', {}, ''),
1510 # POST to account container updating X-Container-Meta-Account-Id
1511 ('204 No Content', {}, '')]))
1512 resp = Request.blank('/auth/v2/act',
1513 environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
1514 headers={'X-Auth-Admin-User': '.super_admin',
1515 'X-Auth-Admin-Key': 'supertest',
1516 'X-Account-Suffix': 'test-suffix'}
1517 ).get_response(self.test_auth)
1518 self.assertEquals(resp.status_int, 201)
1519 self.assertEquals(conn.request_path, '/v1/AUTH_test-suffix')
1520 self.assertEquals(self.test_auth.app.calls, 5)
1521 self.assertEquals(conn.calls, 1)
1522
1523 def test_put_account_fail_bad_creds(self):
1524 self.test_auth.app = FakeApp(iter([
1525 # GET of user object
1526 ('404 Not Found', {}, '')]))
1527 resp = Request.blank('/auth/v2/act',
1528 environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
1529 headers={'X-Auth-Admin-User': 'super:admin',
1530 'X-Auth-Admin-Key': 'supertest'},
1531 ).get_response(self.test_auth)
1532 self.assertEquals(resp.status_int, 403)
1533 self.assertEquals(self.test_auth.app.calls, 1)
1534
1535 self.test_auth.app = FakeApp(iter([
1536 # GET of user object (account admin, but not reseller admin)
1537 ('200 Ok', {}, json.dumps({"groups": [{"name": "act:adm"},
1538 {"name": "test"}, {"name": ".admin"}],
1539 "auth": "plaintext:key"}))]))
1540 resp = Request.blank('/auth/v2/act',
1541 environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
1542 headers={'X-Auth-Admin-User': 'act:adm',
1543 'X-Auth-Admin-Key': 'key'},
1544 ).get_response(self.test_auth)
1545 self.assertEquals(resp.status_int, 403)
1546 self.assertEquals(self.test_auth.app.calls, 1)
1547
1548 self.test_auth.app = FakeApp(iter([
1549 # GET of user object (regular user)
1550 ('200 Ok', {}, json.dumps({"groups": [{"name": "act:usr"},
1551 {"name": "test"}], "auth": "plaintext:key"}))]))
1552 resp = Request.blank('/auth/v2/act',
1553 environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
1554 headers={'X-Auth-Admin-User': 'act:usr',
1555 'X-Auth-Admin-Key': 'key'},
1556 ).get_response(self.test_auth)
1557 self.assertEquals(resp.status_int, 403)
1558 self.assertEquals(self.test_auth.app.calls, 1)
1559
1560 def test_put_account_fail_invalid_account_name(self):
1561 resp = Request.blank('/auth/v2/.act',
1562 environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
1563 headers={'X-Auth-Admin-User': '.super_admin',
1564 'X-Auth-Admin-Key': 'supertest'},
1565 ).get_response(self.test_auth)
1566 self.assertEquals(resp.status_int, 400)
1567
1568 def test_put_account_fail_on_initial_account_head(self):
1569 self.test_auth.app = FakeApp(iter([
1570 # Initial HEAD of account container to check for pre-existence
1571 ('503 Service Unavailable', {}, '')]))
1572 resp = Request.blank('/auth/v2/act',
1573 environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
1574 headers={'X-Auth-Admin-User': '.super_admin',
1575 'X-Auth-Admin-Key': 'supertest'}
1576 ).get_response(self.test_auth)
1577 self.assertEquals(resp.status_int, 500)
1578 self.assertEquals(self.test_auth.app.calls, 1)
1579
1580 def test_put_account_fail_on_account_marker_put(self):
1581 self.test_auth.app = FakeApp(iter([
1582 # Initial HEAD of account container to check for pre-existence
1583 ('404 Not Found', {}, ''),
1584 # PUT of account container
1585 ('503 Service Unavailable', {}, '')]))
1586 resp = Request.blank('/auth/v2/act',
1587 environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
1588 headers={'X-Auth-Admin-User': '.super_admin',
1589 'X-Auth-Admin-Key': 'supertest'}
1590 ).get_response(self.test_auth)
1591 self.assertEquals(resp.status_int, 500)
1592 self.assertEquals(self.test_auth.app.calls, 2)
1593
1594 def test_put_account_fail_on_storage_account_put(self):
1595 conn = FakeConn(iter([
1596 # PUT of storage account itself
1597 ('503 Service Unavailable', {}, '')]))
1598 self.test_auth.get_conn = lambda: conn
1599 self.test_auth.app = FakeApp(iter([
1600 # Initial HEAD of account container to check for pre-existence
1601 ('404 Not Found', {}, ''),
1602 # PUT of account container
1603 ('204 No Content', {}, '')]))
1604 resp = Request.blank('/auth/v2/act',
1605 environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
1606 headers={'X-Auth-Admin-User': '.super_admin',
1607 'X-Auth-Admin-Key': 'supertest'}
1608 ).get_response(self.test_auth)
1609 self.assertEquals(resp.status_int, 500)
1610 self.assertEquals(conn.calls, 1)
1611 self.assertEquals(self.test_auth.app.calls, 2)
1612
1613 def test_put_account_fail_on_account_id_mapping(self):
1614 conn = FakeConn(iter([
1615 # PUT of storage account itself
1616 ('201 Created', {}, '')]))
1617 self.test_auth.get_conn = lambda: conn
1618 self.test_auth.app = FakeApp(iter([
1619 # Initial HEAD of account container to check for pre-existence
1620 ('404 Not Found', {}, ''),
1621 # PUT of account container
1622 ('204 No Content', {}, ''),
1623 # PUT of .account_id mapping object
1624 ('503 Service Unavailable', {}, '')]))
1625 resp = Request.blank('/auth/v2/act',
1626 environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
1627 headers={'X-Auth-Admin-User': '.super_admin',
1628 'X-Auth-Admin-Key': 'supertest'}
1629 ).get_response(self.test_auth)
1630 self.assertEquals(resp.status_int, 500)
1631 self.assertEquals(conn.calls, 1)
1632 self.assertEquals(self.test_auth.app.calls, 3)
1633
1634 def test_put_account_fail_on_services_object(self):
1635 conn = FakeConn(iter([
1636 # PUT of storage account itself
1637 ('201 Created', {}, '')]))
1638 self.test_auth.get_conn = lambda: conn
1639 self.test_auth.app = FakeApp(iter([
1640 # Initial HEAD of account container to check for pre-existence
1641 ('404 Not Found', {}, ''),
1642 # PUT of account container
1643 ('204 No Content', {}, ''),
1644 # PUT of .account_id mapping object
1645 ('204 No Content', {}, ''),
1646 # PUT of .services object
1647 ('503 Service Unavailable', {}, '')]))
1648 resp = Request.blank('/auth/v2/act',
1649 environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
1650 headers={'X-Auth-Admin-User': '.super_admin',
1651 'X-Auth-Admin-Key': 'supertest'}
1652 ).get_response(self.test_auth)
1653 self.assertEquals(resp.status_int, 500)
1654 self.assertEquals(conn.calls, 1)
1655 self.assertEquals(self.test_auth.app.calls, 4)
1656
1657 def test_put_account_fail_on_post_mapping(self):
1658 conn = FakeConn(iter([
1659 # PUT of storage account itself
1660 ('201 Created', {}, '')]))
1661 self.test_auth.get_conn = lambda: conn
1662 self.test_auth.app = FakeApp(iter([
1663 # Initial HEAD of account container to check for pre-existence
1664 ('404 Not Found', {}, ''),
1665 # PUT of account container
1666 ('204 No Content', {}, ''),
1667 # PUT of .account_id mapping object
1668 ('204 No Content', {}, ''),
1669 # PUT of .services object
1670 ('204 No Content', {}, ''),
1671 # POST to account container updating X-Container-Meta-Account-Id
1672 ('503 Service Unavailable', {}, '')]))
1673 resp = Request.blank('/auth/v2/act',
1674 environ={'REQUEST_METHOD': 'PUT', 'swift.cache': FakeMemcache()},
1675 headers={'X-Auth-Admin-User': '.super_admin',
1676 'X-Auth-Admin-Key': 'supertest'}
1677 ).get_response(self.test_auth)
1678 self.assertEquals(resp.status_int, 500)
1679 self.assertEquals(conn.calls, 1)
1680 self.assertEquals(self.test_auth.app.calls, 5)
1681
1682 def test_delete_account_success(self):
1683 conn = FakeConn(iter([
1684 # DELETE of storage account itself
1685 ('204 No Content', {}, '')]))
1686 self.test_auth.get_conn = lambda x: conn
1687 self.test_auth.app = FakeApp(iter([
1688 # Account's container listing, checking for users
1689 ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
1690 json.dumps([
1691 {"name": ".services", "hash": "etag", "bytes": 112,
1692 "content_type": "application/octet-stream",
1693 "last_modified": "2010-12-03T17:16:27.618110"}])),
1694 # Account's container listing, checking for users (continuation)
1695 ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'),
1696 # GET the .services object
1697 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
1698 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
1699 # DELETE the .services object
1700 ('204 No Content', {}, ''),
1701 # DELETE the .account_id mapping object
1702 ('204 No Content', {}, ''),
1703 # DELETE the account container
1704 ('204 No Content', {}, '')]))
1705 resp = Request.blank('/auth/v2/act',
1706 environ={'REQUEST_METHOD': 'DELETE',
1707 'swift.cache': FakeMemcache()},
1708 headers={'X-Auth-Admin-User': '.super_admin',
1709 'X-Auth-Admin-Key': 'supertest'}
1710 ).get_response(self.test_auth)
1711 self.assertEquals(resp.status_int, 204)
1712 self.assertEquals(self.test_auth.app.calls, 6)
1713 self.assertEquals(conn.calls, 1)
1714
1715 def test_delete_account_success_missing_services(self):
1716 self.test_auth.app = FakeApp(iter([
1717 # Account's container listing, checking for users
1718 ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
1719 json.dumps([
1720 {"name": ".services", "hash": "etag", "bytes": 112,
1721 "content_type": "application/octet-stream",
1722 "last_modified": "2010-12-03T17:16:27.618110"}])),
1723 # Account's container listing, checking for users (continuation)
1724 ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'),
1725 # GET the .services object
1726 ('404 Not Found', {}, ''),
1727 # DELETE the .account_id mapping object
1728 ('204 No Content', {}, ''),
1729 # DELETE the account container
1730 ('204 No Content', {}, '')]))
1731 resp = Request.blank('/auth/v2/act',
1732 environ={'REQUEST_METHOD': 'DELETE',
1733 'swift.cache': FakeMemcache()},
1734 headers={'X-Auth-Admin-User': '.super_admin',
1735 'X-Auth-Admin-Key': 'supertest'}
1736 ).get_response(self.test_auth)
1737 self.assertEquals(resp.status_int, 204)
1738 self.assertEquals(self.test_auth.app.calls, 5)
1739
1740 def test_delete_account_success_missing_storage_account(self):
1741 conn = FakeConn(iter([
1742 # DELETE of storage account itself
1743 ('404 Not Found', {}, '')]))
1744 self.test_auth.get_conn = lambda x: conn
1745 self.test_auth.app = FakeApp(iter([
1746 # Account's container listing, checking for users
1747 ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'},
1748 json.dumps([
1749 {"name": ".services", "hash": "etag", "bytes": 112,
1750 "content_type": "application/octet-stream",
1751 "last_modified": "2010-12-03T17:16:27.618110"}])),
1752 # Account's container listing, checking for users (continuation)
1753 ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, '[]'),
1754 # GET the .services object
1755 ('200 Ok', {}, json.dumps({"storage": {"default": "local",
1756 "local": "http://127.0.0.1:8080/v1/AUTH_cfa"}})),
1757 # DELETE the .services object
1758 ('204 No Content', {}, ''),
The diff has been truncated for viewing.