From 98c201043c52893f3ec23b990b14c3441a2f77b3 Mon Sep 17 00:00:00 2001 From: FUJITA Tomonori Date: Wed, 19 Jan 2011 09:56:58 +0900 Subject: [PATCH 1/4] swauth: add s3api support This changes handle_put_user() to put 'x-object-meta-account-id' to a user object. The metadata includes an cfaccount like x-container-meta-account-id. The above enables swauth to avoid issuing two HTTP requests per single S3 request, that is, swauth get the password and cfaccount from the account and user by issuing 'GET /v1/(auth_account)/(account)/(user)' If swauth can't get 'x-object-meta-account-id' metadata from a user object (the existing user objects), it issues 'GET /v1/(auth_account)/(account)' to get the cfaccount. --- swift/common/middleware/swauth.py | 50 +++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/swift/common/middleware/swauth.py b/swift/common/middleware/swauth.py index 105098c807..29dfa6228c 100644 --- a/swift/common/middleware/swauth.py +++ b/swift/common/middleware/swauth.py @@ -23,6 +23,9 @@ from traceback import format_exc from urllib import quote, unquote from urlparse import urlparse from uuid import uuid4 +from hashlib import md5, sha1 +import hmac +import base64 from eventlet.timeout import Timeout from webob import Response, Request @@ -123,8 +126,9 @@ class Swauth(object): env['HTTP_X_CF_TRANS_ID'] = 'tx' + str(uuid4()) if env.get('PATH_INFO', '').startswith(self.auth_prefix): return self.handle(env, start_response) + s3 = env.get('HTTP_AUTHORIZATION') token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) - if token and token.startswith(self.reseller_prefix): + if s3 or (token and token.startswith(self.reseller_prefix)): # Note: Empty reseller_prefix will match all tokens. groups = self.get_groups(env, token) if groups: @@ -192,6 +196,40 @@ class Swauth(object): expires, groups = cached_auth_data if expires < time(): groups = None + + if env.get('HTTP_AUTHORIZATION'): + account, user, sign = env['HTTP_AUTHORIZATION'].split(' ')[1].split(':') + path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user)) + resp = self.make_request(env, 'GET', path).get_response(self.app) + if resp.status_int // 100 != 2: + return None + + if 'x-object-meta-account-id' in resp.headers: + account_id = resp.headers['x-object-meta-account-id'] + else: + path = quote('/v1/%s/%s' % (self.auth_account, account)) + resp2 = self.make_request(env, 'GET', path).get_response(self.app) + if resp2.status_int // 100 != 2: + return None + account_id = resp2.headers['x-container-meta-account-id'] + + path = env['PATH_INFO'] + env['PATH_INFO'] = path.replace("%s:%s" % (account, user), account_id, 1) + detail = json.loads(resp.body) + + password = detail['auth'].split(':')[-1] + msg = base64.urlsafe_b64decode(unquote(token)) + s = base64.encodestring(hmac.new(detail['auth'].split(':')[-1], msg, sha1).digest()).strip() + if s != sign: + return None + + groups = [g['name'] for g in detail['groups']] + if '.admin' in groups: + groups.remove('.admin') + groups.append(account_id) + groups = ','.join(groups) + return groups + if not groups: path = quote('/v1/%s/.token_%s/%s' % (self.auth_account, token[-1], token)) @@ -839,6 +877,13 @@ class Swauth(object): return HTTPForbidden(request=req) elif not self.is_account_admin(req, account): return HTTPForbidden(request=req) + + path = quote('/v1/%s/%s' % (self.auth_account, account)) + resp = self.make_request(req.environ, 'GET', path).get_response(self.app) + if resp.status_int // 100 != 2: + raise Exception('Could not create user object: %s %s' % + (path, resp.status)) + headers={'X-Object-Meta-Account-Id': '%s' % resp.headers['x-container-meta-account-id']} # Create the object in the main auth account (this object represents # the user) path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user)) @@ -847,9 +892,10 @@ class Swauth(object): groups.append('.admin') if reseller_admin: groups.append('.reseller_admin') + resp = self.make_request(req.environ, 'PUT', path, json.dumps({'auth': 'plaintext:%s' % key, - 'groups': [{'name': g} for g in groups]})).get_response(self.app) + 'groups': [{'name': g} for g in groups]}), headers=headers).get_response(self.app) if resp.status_int == 404: return HTTPNotFound(request=req) if resp.status_int // 100 != 2: From ba8255affcae6f8ed27071121ead1e755ed56ca5 Mon Sep 17 00:00:00 2001 From: FUJITA Tomonori Date: Wed, 19 Jan 2011 15:23:29 +0900 Subject: [PATCH 2/4] swauth: update the unit tests for s3api changes --- test/unit/common/middleware/test_swauth.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/unit/common/middleware/test_swauth.py b/test/unit/common/middleware/test_swauth.py index 2e4d958a44..c1dff69599 100644 --- a/test/unit/common/middleware/test_swauth.py +++ b/test/unit/common/middleware/test_swauth.py @@ -2561,6 +2561,7 @@ class TestAuth(unittest.TestCase): def test_put_user_regular_success(self): self.test_auth.app = FakeApp(iter([ + ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), # PUT of user object ('201 Created', {}, '')])) resp = Request.blank('/auth/v2/act/usr', @@ -2570,13 +2571,14 @@ class TestAuth(unittest.TestCase): 'X-Auth-User-Key': 'key'} ).get_response(self.test_auth) self.assertEquals(resp.status_int, 201) - self.assertEquals(self.test_auth.app.calls, 1) + self.assertEquals(self.test_auth.app.calls, 2) self.assertEquals(json.loads(self.test_auth.app.request.body), {"groups": [{"name": "act:usr"}, {"name": "act"}], "auth": "plaintext:key"}) def test_put_user_account_admin_success(self): self.test_auth.app = FakeApp(iter([ + ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), # PUT of user object ('201 Created', {}, '')])) resp = Request.blank('/auth/v2/act/usr', @@ -2587,7 +2589,7 @@ class TestAuth(unittest.TestCase): 'X-Auth-User-Admin': 'true'} ).get_response(self.test_auth) self.assertEquals(resp.status_int, 201) - self.assertEquals(self.test_auth.app.calls, 1) + self.assertEquals(self.test_auth.app.calls, 2) self.assertEquals(json.loads(self.test_auth.app.request.body), {"groups": [{"name": "act:usr"}, {"name": "act"}, {"name": ".admin"}], @@ -2595,6 +2597,7 @@ class TestAuth(unittest.TestCase): def test_put_user_reseller_admin_success(self): self.test_auth.app = FakeApp(iter([ + ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), # PUT of user object ('201 Created', {}, '')])) resp = Request.blank('/auth/v2/act/usr', @@ -2605,7 +2608,7 @@ class TestAuth(unittest.TestCase): 'X-Auth-User-Reseller-Admin': 'true'} ).get_response(self.test_auth) self.assertEquals(resp.status_int, 201) - self.assertEquals(self.test_auth.app.calls, 1) + self.assertEquals(self.test_auth.app.calls, 2) self.assertEquals(json.loads(self.test_auth.app.request.body), {"groups": [{"name": "act:usr"}, {"name": "act"}, {"name": ".admin"}, {"name": ".reseller_admin"}], @@ -2613,6 +2616,7 @@ class TestAuth(unittest.TestCase): def test_put_user_fail_not_found(self): self.test_auth.app = FakeApp(iter([ + ('200 Ok', {'X-Container-Meta-Account-Id': 'AUTH_cfa'}, ''), # PUT of user object ('404 Not Found', {}, '')])) resp = Request.blank('/auth/v2/act/usr', @@ -2622,7 +2626,7 @@ class TestAuth(unittest.TestCase): 'X-Auth-User-Key': 'key'} ).get_response(self.test_auth) self.assertEquals(resp.status_int, 404) - self.assertEquals(self.test_auth.app.calls, 1) + self.assertEquals(self.test_auth.app.calls, 2) def test_put_user_fail(self): self.test_auth.app = FakeApp(iter([ From ea9ccf33b87488d9660e57c64bde0f980fa8617b Mon Sep 17 00:00:00 2001 From: FUJITA Tomonori Date: Wed, 19 Jan 2011 15:23:53 +0900 Subject: [PATCH 3/4] swauth: pep8 fixes --- swift/common/middleware/swauth.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/swift/common/middleware/swauth.py b/swift/common/middleware/swauth.py index 29dfa6228c..0989c30bd5 100644 --- a/swift/common/middleware/swauth.py +++ b/swift/common/middleware/swauth.py @@ -198,7 +198,8 @@ class Swauth(object): groups = None if env.get('HTTP_AUTHORIZATION'): - account, user, sign = env['HTTP_AUTHORIZATION'].split(' ')[1].split(':') + account = env['HTTP_AUTHORIZATION'].split(' ')[1] + account, user, sign = account.split(':') path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user)) resp = self.make_request(env, 'GET', path).get_response(self.app) if resp.status_int // 100 != 2: @@ -208,21 +209,23 @@ class Swauth(object): account_id = resp.headers['x-object-meta-account-id'] else: path = quote('/v1/%s/%s' % (self.auth_account, account)) - resp2 = self.make_request(env, 'GET', path).get_response(self.app) + resp2 = self.make_request(env, 'GET', + path).get_response(self.app) if resp2.status_int // 100 != 2: return None account_id = resp2.headers['x-container-meta-account-id'] path = env['PATH_INFO'] - env['PATH_INFO'] = path.replace("%s:%s" % (account, user), account_id, 1) + env['PATH_INFO'] = path.replace("%s:%s" % (account, user), + account_id, 1) detail = json.loads(resp.body) - password = detail['auth'].split(':')[-1] + password = detail['auth'].split(':')[-1] msg = base64.urlsafe_b64decode(unquote(token)) - s = base64.encodestring(hmac.new(detail['auth'].split(':')[-1], msg, sha1).digest()).strip() + s = base64.encodestring(hmac.new(detail['auth'].split(':')[-1], + msg, sha1).digest()).strip() if s != sign: return None - groups = [g['name'] for g in detail['groups']] if '.admin' in groups: groups.remove('.admin') @@ -883,7 +886,7 @@ class Swauth(object): if resp.status_int // 100 != 2: raise Exception('Could not create user object: %s %s' % (path, resp.status)) - headers={'X-Object-Meta-Account-Id': '%s' % resp.headers['x-container-meta-account-id']} + headers = {'X-Object-Meta-Account-Id': '%s' % resp.headers['x-container-meta-account-id']} # Create the object in the main auth account (this object represents # the user) path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user)) @@ -892,7 +895,6 @@ class Swauth(object): groups.append('.admin') if reseller_admin: groups.append('.reseller_admin') - resp = self.make_request(req.environ, 'PUT', path, json.dumps({'auth': 'plaintext:%s' % key, 'groups': [{'name': g} for g in groups]}), headers=headers).get_response(self.app) From 264fc584b2979e4d6269c51e480efc4399c2f195 Mon Sep 17 00:00:00 2001 From: gholt Date: Mon, 24 Jan 2011 13:09:06 -0800 Subject: [PATCH 4/4] swauth: log s3 in place of token when in use; changed a couple GETs to HEADs; pep8 --- swift/common/middleware/swauth.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/swift/common/middleware/swauth.py b/swift/common/middleware/swauth.py index 0989c30bd5..dd846898bb 100644 --- a/swift/common/middleware/swauth.py +++ b/swift/common/middleware/swauth.py @@ -136,7 +136,8 @@ class Swauth(object): user = groups and groups.split(',', 1)[0] or '' # We know the proxy logs the token, so we augment it just a bit # to also log the authenticated user. - env['HTTP_X_AUTH_TOKEN'] = '%s,%s' % (user, token) + env['HTTP_X_AUTH_TOKEN'] = \ + '%s,%s' % (user, 's3' if s3 else token) env['swift.authorize'] = self.authorize env['swift.clean_acl'] = clean_acl else: @@ -209,7 +210,7 @@ class Swauth(object): account_id = resp.headers['x-object-meta-account-id'] else: path = quote('/v1/%s/%s' % (self.auth_account, account)) - resp2 = self.make_request(env, 'GET', + resp2 = self.make_request(env, 'HEAD', path).get_response(self.app) if resp2.status_int // 100 != 2: return None @@ -882,11 +883,13 @@ class Swauth(object): return HTTPForbidden(request=req) path = quote('/v1/%s/%s' % (self.auth_account, account)) - resp = self.make_request(req.environ, 'GET', path).get_response(self.app) + resp = self.make_request(req.environ, 'HEAD', + path).get_response(self.app) if resp.status_int // 100 != 2: - raise Exception('Could not create user object: %s %s' % + raise Exception('Could not retrieve account id value: %s %s' % (path, resp.status)) - headers = {'X-Object-Meta-Account-Id': '%s' % resp.headers['x-container-meta-account-id']} + headers = {'X-Object-Meta-Account-Id': + resp.headers['x-container-meta-account-id']} # Create the object in the main auth account (this object represents # the user) path = quote('/v1/%s/%s/%s' % (self.auth_account, account, user)) @@ -895,9 +898,10 @@ class Swauth(object): groups.append('.admin') if reseller_admin: groups.append('.reseller_admin') - resp = self.make_request(req.environ, 'PUT', path, json.dumps({'auth': - 'plaintext:%s' % key, - 'groups': [{'name': g} for g in groups]}), headers=headers).get_response(self.app) + resp = self.make_request(req.environ, 'PUT', path, + json.dumps({'auth': 'plaintext:%s' % key, + 'groups': [{'name': g} for g in groups]}), + headers=headers).get_response(self.app) if resp.status_int == 404: return HTTPNotFound(request=req) if resp.status_int // 100 != 2: