From c60c409d61c0dd2e13235a27810954a66de03c81 Mon Sep 17 00:00:00 2001 From: gholt Date: Wed, 15 Sep 2010 14:26:53 -0700 Subject: [PATCH 1/5] devauth-middleware will not set its authorize func unless the token or account starts with the reseller prefix; if its not going to use its authorize func, it will set a deny-by-default func if one is not set already --- swift/common/middleware/auth.py | 23 +++++---- test/unit/common/middleware/test_auth.py | 59 +++++++++++++++++++----- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/swift/common/middleware/auth.py b/swift/common/middleware/auth.py index 70162ac856..9362d7bc2c 100644 --- a/swift/common/middleware/auth.py +++ b/swift/common/middleware/auth.py @@ -45,9 +45,9 @@ class DevAuth(object): validation. For an authenticated request, REMOTE_USER will be set to a comma separated list of the user's groups. """ - groups = None token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) if token and token.startswith(self.reseller_prefix): + groups = None memcache_client = cache_from_env(env) key = '%s/token/%s' % (self.reseller_prefix, token) cached_auth_data = memcache_client.get(key) @@ -68,13 +68,20 @@ class DevAuth(object): groups = resp.getheader('x-auth-groups') memcache_client.set(key, (time(), expiration, groups), timeout=expiration) - env['REMOTE_USER'] = groups - env['swift.authorize'] = self.authorize - env['swift.clean_acl'] = clean_acl - # We know the proxy logs the token, so we augment it just a bit to also - # log the authenticated user. - user = groups and groups.split(',', 1)[0] or '' - env['HTTP_X_AUTH_TOKEN'] = '%s,%s' % (user, token) + env['REMOTE_USER'] = groups + env['swift.authorize'] = self.authorize + env['swift.clean_acl'] = clean_acl + # We know the proxy logs the token, so we augment it just a bit to + # also log the authenticated user. + user = groups and groups.split(',', 1)[0] or '' + env['HTTP_X_AUTH_TOKEN'] = '%s,%s' % (user, token) + else: + version, rest = split_path(env.get('PATH_INFO', ''), 1, 2, True) + if rest and rest.startswith(self.reseller_prefix): + env['swift.authorize'] = self.authorize + env['swift.clean_acl'] = clean_acl + elif 'swift.authorize' not in env: + env['swift.authorize'] = self.denied_response return self.app(env, start_response) def authorize(self, req): diff --git a/test/unit/common/middleware/test_auth.py b/test/unit/common/middleware/test_auth.py index 3127fb2749..402c943620 100644 --- a/test/unit/common/middleware/test_auth.py +++ b/test/unit/common/middleware/test_auth.py @@ -94,6 +94,11 @@ class Logger(object): class FakeApp(object): def __call__(self, env, start_response): + req = Request(env) + if 'swift.authorize' in env: + resp = env['swift.authorize'](req) + if resp: + return resp(env, start_response) return ['204 No Content'] def start_response(*args): @@ -104,6 +109,35 @@ class TestAuth(unittest.TestCase): def setUp(self): self.test_auth = auth.filter_factory({})(FakeApp()) + def test_auth_deny_non_reseller_prefix(self): + old_http_connect = auth.http_connect + try: + auth.http_connect = mock_http_connect(204, + {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'}) + reqenv = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/BLAH_account', + 'HTTP_X_AUTH_TOKEN': 'BLAH_t', 'swift.cache': FakeMemcache()} + result = ''.join(self.test_auth(reqenv, lambda x, y: None)) + self.assert_(result.startswith('401'), result) + self.assertEquals(reqenv['swift.authorize'], + self.test_auth.denied_response) + finally: + auth.http_connect = old_http_connect + + def test_auth_deny_non_reseller_prefix_no_override(self): + old_http_connect = auth.http_connect + try: + auth.http_connect = mock_http_connect(204, + {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'}) + fake_authorize = lambda x: lambda x, y: ['500 Fake'] + reqenv = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/BLAH_account', + 'HTTP_X_AUTH_TOKEN': 'BLAH_t', 'swift.cache': FakeMemcache(), + 'swift.authorize': fake_authorize} + result = ''.join(self.test_auth(reqenv, lambda x, y: None)) + self.assert_(result.startswith('500 Fake'), result) + self.assertEquals(reqenv['swift.authorize'], fake_authorize) + finally: + auth.http_connect = old_http_connect + def test_auth_fail(self): old_http_connect = auth.http_connect try: @@ -121,8 +155,8 @@ class TestAuth(unittest.TestCase): auth.http_connect = mock_http_connect(204, {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'}) result = ''.join(self.test_auth({'REQUEST_METHOD': 'GET', - 'HTTP_X_AUTH_TOKEN': 'AUTH_t', 'swift.cache': FakeMemcache()}, - lambda x, y: None)) + 'PATH_INFO': '/v/AUTH_cfa', 'HTTP_X_AUTH_TOKEN': 'AUTH_t', + 'swift.cache': FakeMemcache()}, lambda x, y: None)) self.assert_(result.startswith('204'), result) finally: auth.http_connect = old_http_connect @@ -134,14 +168,14 @@ class TestAuth(unittest.TestCase): auth.http_connect = mock_http_connect(204, {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'}) result = ''.join(self.test_auth({'REQUEST_METHOD': 'GET', - 'HTTP_X_AUTH_TOKEN': 'AUTH_t', 'swift.cache': fake_memcache}, - lambda x, y: None)) + 'PATH_INFO': '/v/AUTH_cfa', 'HTTP_X_AUTH_TOKEN': 'AUTH_t', + 'swift.cache': fake_memcache}, lambda x, y: None)) self.assert_(result.startswith('204'), result) auth.http_connect = mock_http_connect(404) # Should still be in memcache result = ''.join(self.test_auth({'REQUEST_METHOD': 'GET', - 'HTTP_X_AUTH_TOKEN': 'AUTH_t', 'swift.cache': fake_memcache}, - lambda x, y: None)) + 'PATH_INFO': '/v/AUTH_cfa', 'HTTP_X_AUTH_TOKEN': 'AUTH_t', + 'swift.cache': fake_memcache}, lambda x, y: None)) self.assert_(result.startswith('204'), result) finally: auth.http_connect = old_http_connect @@ -153,8 +187,8 @@ class TestAuth(unittest.TestCase): auth.http_connect = mock_http_connect(204, {'x-auth-ttl': '0', 'x-auth-groups': 'act:usr,act,AUTH_cfa'}) result = ''.join(self.test_auth({'REQUEST_METHOD': 'GET', - 'HTTP_X_AUTH_TOKEN': 'AUTH_t', 'swift.cache': fake_memcache}, - lambda x, y: None)) + 'PATH_INFO': '/v/AUTH_cfa', 'HTTP_X_AUTH_TOKEN': 'AUTH_t', + 'swift.cache': fake_memcache}, lambda x, y: None)) self.assert_(result.startswith('204'), result) auth.http_connect = mock_http_connect(404) # Should still be in memcache, but expired @@ -170,7 +204,8 @@ class TestAuth(unittest.TestCase): try: auth.http_connect = mock_http_connect(204, {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'}) - req = Request.blank('/v/a/c/o', headers={'x-auth-token': 'AUTH_t'}) + req = Request.blank('/v/AUTH_cfa/c/o', + headers={'x-auth-token': 'AUTH_t'}) req.environ['swift.cache'] = FakeMemcache() result = ''.join(self.test_auth(req.environ, start_response)) self.assert_(result.startswith('204'), result) @@ -183,10 +218,10 @@ class TestAuth(unittest.TestCase): try: auth.http_connect = mock_http_connect(204, {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'}) - req = Request.blank('/v/a/c/o') + req = Request.blank('/v/AUTH_cfa/c/o') req.environ['swift.cache'] = FakeMemcache() result = ''.join(self.test_auth(req.environ, start_response)) - self.assert_(result.startswith('204'), result) + self.assert_(result.startswith('401'), result) self.assert_(not req.remote_user, req.remote_user) finally: auth.http_connect = old_http_connect @@ -196,7 +231,7 @@ class TestAuth(unittest.TestCase): try: auth.http_connect = mock_http_connect(204, {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'}) - req = Request.blank('/v/a/c/o', + req = Request.blank('/v/AUTH_cfa/c/o', headers={'x-storage-token': 'AUTH_t'}) req.environ['swift.cache'] = FakeMemcache() result = ''.join(self.test_auth(req.environ, start_response)) From 01059884b3059b0ab30b2de5918471dca4bba850 Mon Sep 17 00:00:00 2001 From: gholt Date: Thu, 16 Sep 2010 11:14:09 -0700 Subject: [PATCH 2/5] Update to better support no-reseller-prefix and multiple auth middleware --- swift/common/middleware/auth.py | 20 +++++++++-- test/unit/common/middleware/test_auth.py | 45 +++++++++++++++++++++++- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/swift/common/middleware/auth.py b/swift/common/middleware/auth.py index 9362d7bc2c..f5e7a2c9d0 100644 --- a/swift/common/middleware/auth.py +++ b/swift/common/middleware/auth.py @@ -63,7 +63,15 @@ class DevAuth(object): resp.read() conn.close() if resp.status // 100 != 2: - return HTTPUnauthorized()(env, start_response) + if self.reseller_prefix: + return HTTPUnauthorized()(env, start_response) + else: + # If we have no reseller prefix, we can't deny the + # request just yet because another auth middleware + # might be able to approve. + if 'swift.authorize' not in env: + env['swift.authorize'] = self.denied_response + return self.app(env, start_response) expiration = float(resp.getheader('x-auth-ttl')) groups = resp.getheader('x-auth-groups') memcache_client.set(key, (time(), expiration, groups), @@ -78,8 +86,14 @@ class DevAuth(object): else: version, rest = split_path(env.get('PATH_INFO', ''), 1, 2, True) if rest and rest.startswith(self.reseller_prefix): - env['swift.authorize'] = self.authorize - env['swift.clean_acl'] = clean_acl + # If we don't have a reseller prefix we have no way of knowing + # if we should be handling the request, so we only set + # swift.authorize if it isn't set already (or we have a + # reseller prefix that matches so we know we should handle the + # request). + if self.reseller_prefix or 'swift.authorize' not in env: + env['swift.authorize'] = self.authorize + env['swift.clean_acl'] = clean_acl elif 'swift.authorize' not in env: env['swift.authorize'] = self.denied_response return self.app(env, start_response) diff --git a/test/unit/common/middleware/test_auth.py b/test/unit/common/middleware/test_auth.py index 402c943620..1f5b0aaffc 100644 --- a/test/unit/common/middleware/test_auth.py +++ b/test/unit/common/middleware/test_auth.py @@ -90,10 +90,15 @@ class Logger(object): _, exc, _ = sys.exc_info() self.exception_value = (msg, '%s %s' % (exc.__class__.__name__, str(exc)), args, kwargs) -# tests + class FakeApp(object): + + def __init__(self): + self.i_was_called = False + def __call__(self, env, start_response): + self.i_was_called = True req = Request(env) if 'swift.authorize' in env: resp = env['swift.authorize'](req) @@ -101,6 +106,7 @@ class FakeApp(object): return resp(env, start_response) return ['204 No Content'] + def start_response(*args): pass @@ -138,6 +144,43 @@ class TestAuth(unittest.TestCase): finally: auth.http_connect = old_http_connect + def test_auth_no_reseller_prefix_deny(self): + # Ensures that when we have no reseller prefix, we don't deny a request + # outright but set up a denial swift.authorize and pass the request on + # down the chain. + old_http_connect = auth.http_connect + try: + local_app = FakeApp() + local_auth = \ + auth.filter_factory({'reseller_prefix': ''})(local_app) + auth.http_connect = mock_http_connect(404) + reqenv = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/account', + 'HTTP_X_AUTH_TOKEN': 't', 'swift.cache': FakeMemcache()} + result = ''.join(local_auth(reqenv, lambda x, y: None)) + self.assert_(result.startswith('401'), result) + self.assert_(local_app.i_was_called) + self.assertEquals(reqenv['swift.authorize'], + local_auth.denied_response) + finally: + auth.http_connect = old_http_connect + + def test_auth_no_reseller_prefix_no_token(self): + # Check that normally we set up a call back to our authorize. + local_auth = \ + auth.filter_factory({'reseller_prefix': ''})(FakeApp()) + reqenv = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/account', + 'swift.cache': FakeMemcache()} + result = ''.join(local_auth(reqenv, lambda x, y: None)) + self.assert_(result.startswith('401'), result) + self.assertEquals(reqenv['swift.authorize'], local_auth.authorize) + # Now make sure we don't override an existing swift.authorize when we + # have no reseller prefix. + local_authorize = lambda req: None + reqenv['swift.authorize'] = local_authorize + result = ''.join(local_auth(reqenv, lambda x, y: None)) + self.assert_(result.startswith('204'), result) + self.assertEquals(reqenv['swift.authorize'], local_authorize) + def test_auth_fail(self): old_http_connect = auth.http_connect try: From d5770ee21401468c49391eee3e8e8a724e92425f Mon Sep 17 00:00:00 2001 From: Clay Gerrard Date: Thu, 16 Sep 2010 16:44:44 -0500 Subject: [PATCH 3/5] trying to make sense of auth middleware and reseller prefix --- doc/source/development_auth.rst | 7 +- swift/auth/server.py | 4 +- swift/common/middleware/auth.py | 109 ++++++++++++++++++-------------- test/unit/auth/test_server.py | 3 +- 4 files changed, 70 insertions(+), 53 deletions(-) diff --git a/doc/source/development_auth.rst b/doc/source/development_auth.rst index 410312f3db..afbe776bab 100644 --- a/doc/source/development_auth.rst +++ b/doc/source/development_auth.rst @@ -60,9 +60,10 @@ Example Authentication with DevAuth: * The external DevAuth server responds with "X-Auth-Groups: test:tester,test,AUTH_storage_xyz" * Now this user will have full access (via authorization procedures later) - to the AUTH_storage_xyz Swift storage account and access to other storage - accounts with the same `AUTH_` reseller prefix and has an ACL specifying - at least one of those three groups returned. + to the AUTH_storage_xyz Swift storage account and access to containers in + other storage accounts, provided the storage account begins with the same + `AUTH_` reseller prefix and the container has an ACL specifying at least + one of those three groups returned. Authorization is performed through callbacks by the Swift Proxy server to the WSGI environment's swift.authorize value, if one is set. The swift.authorize diff --git a/swift/auth/server.py b/swift/auth/server.py index e8214db354..24acda6196 100644 --- a/swift/auth/server.py +++ b/swift/auth/server.py @@ -454,7 +454,7 @@ YOU HAVE A FEW OPTIONS: if create_reseller_admin and ( request.headers.get('X-Auth-Admin-User') != '.super_admin' or request.headers.get('X-Auth-Admin-Key') != self.super_admin_key): - return HTTPForbidden(request=request) + return HTTPUnauthorized(request=request) create_account_admin = \ request.headers.get('x-auth-user-admin') == 'true' if create_account_admin and \ @@ -484,7 +484,7 @@ YOU HAVE A FEW OPTIONS: """ if request.headers.get('X-Auth-Admin-User') != '.super_admin' or \ request.headers.get('X-Auth-Admin-Key') != self.super_admin_key: - return HTTPForbidden(request=request) + return HTTPUnauthorized(request=request) result = self.recreate_accounts() return Response(result, 200, request=request) diff --git a/swift/common/middleware/auth.py b/swift/common/middleware/auth.py index f5e7a2c9d0..05939d06ac 100644 --- a/swift/common/middleware/auth.py +++ b/swift/common/middleware/auth.py @@ -20,7 +20,7 @@ from webob.exc import HTTPForbidden, HTTPUnauthorized from swift.common.bufferedhttp import http_connect_raw as http_connect from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed -from swift.common.utils import cache_from_env, split_path +from swift.common.utils import cache_from_env, split_path, TRUE_VALUES class DevAuth(object): @@ -35,9 +35,33 @@ class DevAuth(object): self.auth_host = conf.get('ip', '127.0.0.1') self.auth_port = int(conf.get('port', 11000)) self.ssl = \ - conf.get('ssl', 'false').lower() in ('true', 'on', '1', 'yes') + conf.get('ssl', 'false').lower() in TRUE_VALUES self.timeout = int(conf.get('node_timeout', 10)) + def get_groups(self, token): + memcache_client = cache_from_env(env) + key = '%s/token/%s' % (self.reseller_prefix, token) + cached_auth_data = memcache_client.get(key) + if cached_auth_data: + start, expiration, groups = cached_auth_data + if time() - start > expiration: + groups = None + if not groups: + with Timeout(self.timeout): + conn = http_connect(self.auth_host, self.auth_port, 'GET', + '/token/%s' % token, ssl=self.ssl) + resp = conn.getresponse() + resp.read() + conn.close() + if resp.status // 100 != 2: + return None + + expiration = float(resp.getheader('x-auth-ttl')) + groups = resp.getheader('x-auth-groups') + memcache_client.set(key, (time(), expiration, groups), + timeout=expiration) + return groups + def __call__(self, env, start_response): """ Accepts a standard WSGI application call, authenticating the request @@ -45,57 +69,48 @@ class DevAuth(object): validation. For an authenticated request, REMOTE_USER will be set to a comma separated list of the user's groups. """ + token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) - if token and token.startswith(self.reseller_prefix): - groups = None - memcache_client = cache_from_env(env) - key = '%s/token/%s' % (self.reseller_prefix, token) - cached_auth_data = memcache_client.get(key) - if cached_auth_data: - start, expiration, groups = cached_auth_data - if time() - start > expiration: - groups = None - if not groups: - with Timeout(self.timeout): - conn = http_connect(self.auth_host, self.auth_port, 'GET', - '/token/%s' % token, ssl=self.ssl) - resp = conn.getresponse() - resp.read() - conn.close() - if resp.status // 100 != 2: - if self.reseller_prefix: - return HTTPUnauthorized()(env, start_response) - else: - # If we have no reseller prefix, we can't deny the - # request just yet because another auth middleware - # might be able to approve. - if 'swift.authorize' not in env: - env['swift.authorize'] = self.denied_response - return self.app(env, start_response) - expiration = float(resp.getheader('x-auth-ttl')) - groups = resp.getheader('x-auth-groups') - memcache_client.set(key, (time(), expiration, groups), - timeout=expiration) - env['REMOTE_USER'] = groups + + if not self.reseller_prefix: + # all requests belong to me + if token: + # I should attempt to auth any token + groups = self.get_groups(token) + else: + groups = None # no token is same as an unauthorized token + if groups: + env['REMOTE_USER'] = groups + user = groups and groups.split(',', 1)[0] or '' + env['HTTP_X_AUTH_TOKEN'] = '%s,%s' % (user, token) env['swift.authorize'] = self.authorize env['swift.clean_acl'] = clean_acl - # We know the proxy logs the token, so we augment it just a bit to - # also log the authenticated user. - user = groups and groups.split(',', 1)[0] or '' - env['HTTP_X_AUTH_TOKEN'] = '%s,%s' % (user, token) else: - version, rest = split_path(env.get('PATH_INFO', ''), 1, 2, True) - if rest and rest.startswith(self.reseller_prefix): - # If we don't have a reseller prefix we have no way of knowing - # if we should be handling the request, so we only set - # swift.authorize if it isn't set already (or we have a - # reseller prefix that matches so we know we should handle the - # request). - if self.reseller_prefix or 'swift.authorize' not in env: + # as a reseller, I must respect that just can my auth can't provide + # groups for a token, others may + if token and token.startswith(self.reseller_prefix):: + # attempt to auth my token with my auth server + groups = self.get_groups(token) + if groups: + # authenticated! + env['REMOTE_USER'] = groups + user = groups and groups.split(',', 1)[0] or '' + env['HTTP_X_AUTH_TOKEN'] = '%s,%s' % (user, token) + env['swift.authorize'] = self.authorize + env['swift.clean_acl'] = clean_acl + else: + # I can't claim this token, but I might claim the annoynomous request + version, rest = split_path(env.get('PATH_INFO', ''), 1, 2, True) + if rest and rest.startswith(self.reseller_prefix): + # annoynomous access to my reseller's accounts env['swift.authorize'] = self.authorize env['swift.clean_acl'] = clean_acl - elif 'swift.authorize' not in env: - env['swift.authorize'] = self.denied_response + else: + # not my token, not my account + # good idea regardless... + if 'swift.authorize' not in env: + env['swift.authorize'] = self.denied_response + return self.app(env, start_response) def authorize(self, req): diff --git a/test/unit/auth/test_server.py b/test/unit/auth/test_server.py index 6ab8b76095..d63f843abe 100644 --- a/test/unit/auth/test_server.py +++ b/test/unit/auth/test_server.py @@ -685,7 +685,8 @@ class TestAuthServer(unittest.TestCase): conf = {'swift_dir': self.testdir, 'log_name': 'auth'} self.assertRaises(ValueError, auth_server.AuthController, conf) conf['super_admin_key'] = 'testkey' - auth_server.AuthController(conf) + controller = auth_server.AuthController(conf) + self.assertEquals(controller.super_admin_key, conf['super_admin_key']) def test_add_storage_account(self): auth_server.http_connect = fake_http_connect(201) From e911634deab4c749efb07dd44db87bbd5a1849fe Mon Sep 17 00:00:00 2001 From: Clay Gerrard Date: Fri, 17 Sep 2010 11:03:07 -0500 Subject: [PATCH 4/5] after I was sure all cases were covered, I rearranged code to be more like greg's; added comments, and tests --- swift/common/middleware/auth.py | 143 +++++++++++++---------- test/unit/common/middleware/test_auth.py | 20 ++++ 2 files changed, 100 insertions(+), 63 deletions(-) diff --git a/swift/common/middleware/auth.py b/swift/common/middleware/auth.py index 05939d06ac..b191dbee6b 100644 --- a/swift/common/middleware/auth.py +++ b/swift/common/middleware/auth.py @@ -38,14 +38,83 @@ class DevAuth(object): conf.get('ssl', 'false').lower() in TRUE_VALUES self.timeout = int(conf.get('node_timeout', 10)) - def get_groups(self, token): - memcache_client = cache_from_env(env) - key = '%s/token/%s' % (self.reseller_prefix, token) - cached_auth_data = memcache_client.get(key) - if cached_auth_data: - start, expiration, groups = cached_auth_data - if time() - start > expiration: - groups = None + def __call__(self, env, start_response): + """ + Accepts a standard WSGI application call, authenticating the request + and installing callback hooks for authorization and ACL header + validation. For an authenticated request, REMOTE_USER will be set to a + comma separated list of the user's groups. + + With out a reseller prefix, I act as the default auth service for + all requests, but I won't overwrite swift.authorize unless my auth + explictly grants groups to this token + + As a reseller, I must respect that my auth server is not + authorative for all tokens, but I can set swift.authorize to + denied_reponse if it's not already set + """ + token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) + if token and token.startswith(self.reseller_prefix): + # N.B. no reseller_prefix will match all tokens! + # attempt to auth my token with my auth server + groups = self.get_groups(token, + memcache_client=cache_from_env(env)) + if groups: + env['REMOTE_USER'] = groups + user = groups and groups.split(',', 1)[0] or '' + env['HTTP_X_AUTH_TOKEN'] = '%s,%s' % (user, token) + env['swift.authorize'] = self.authorize + env['swift.clean_acl'] = clean_acl + else: + # unauthorized token + if self.reseller_prefix: + # because I own this token, I can deny it outright + return HTTPUnauthorized()(env, start_response) + elif 'swift.authorize' not in env: + # default auth won't over-write swift.authorize + env['swift.authorize'] = self.denied_response + else: + if self.reseller_prefix: + # As a reseller, I would like to be calledback for annoynomous + # access to my accounts + version, rest = split_path(env.get('PATH_INFO', ''), + 1, 2, True) + if rest and rest.startswith(self.reseller_prefix): + # handle annoynomous access to my reseller's accounts + env['swift.authorize'] = self.authorize + env['swift.clean_acl'] = clean_acl + elif 'swift.authorize' not in env: + # not my token, not my account, I can't authorize this + # request, deny all is a good idea if not already set... + env['swift.authorize'] = self.denied_response + elif 'swift.authorize' not in env: + # As a default auth, I'm willing to handle annoynomous requests + # for all accounts, but I won't over-write swift.authorize + env['swift.authorize'] = self.authorize + env['swift.clean_acl'] = clean_acl + + return self.app(env, start_response) + + def get_groups(self, token, memcache_client=None): + """ + Get groups for the given token, may use a memcache_client if set, and + update cache/expire old values; otherwise call auth_host and return + 'x-auth-groups' + """ + groups = None + if memcache_client: + key = '%s/token/%s' % (self.reseller_prefix, token) + cached_auth_data = memcache_client.get(key) + if cached_auth_data: + start, expiration, groups = cached_auth_data + if time() - start > expiration: + groups = None + + def set_memcache(expiration, groups, key=key): + memcache_client.set(key, (time(), expiration, groups), + timeout=expiration) + else: + set_memcache = lambda *args: None if not groups: with Timeout(self.timeout): conn = http_connect(self.auth_host, self.auth_port, 'GET', @@ -55,63 +124,10 @@ class DevAuth(object): conn.close() if resp.status // 100 != 2: return None - expiration = float(resp.getheader('x-auth-ttl')) groups = resp.getheader('x-auth-groups') - memcache_client.set(key, (time(), expiration, groups), - timeout=expiration) - return groups - - def __call__(self, env, start_response): - """ - Accepts a standard WSGI application call, authenticating the request - and installing callback hooks for authorization and ACL header - validation. For an authenticated request, REMOTE_USER will be set to a - comma separated list of the user's groups. - """ - - token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) - - if not self.reseller_prefix: - # all requests belong to me - if token: - # I should attempt to auth any token - groups = self.get_groups(token) - else: - groups = None # no token is same as an unauthorized token - if groups: - env['REMOTE_USER'] = groups - user = groups and groups.split(',', 1)[0] or '' - env['HTTP_X_AUTH_TOKEN'] = '%s,%s' % (user, token) - env['swift.authorize'] = self.authorize - env['swift.clean_acl'] = clean_acl - else: - # as a reseller, I must respect that just can my auth can't provide - # groups for a token, others may - if token and token.startswith(self.reseller_prefix):: - # attempt to auth my token with my auth server - groups = self.get_groups(token) - if groups: - # authenticated! - env['REMOTE_USER'] = groups - user = groups and groups.split(',', 1)[0] or '' - env['HTTP_X_AUTH_TOKEN'] = '%s,%s' % (user, token) - env['swift.authorize'] = self.authorize - env['swift.clean_acl'] = clean_acl - else: - # I can't claim this token, but I might claim the annoynomous request - version, rest = split_path(env.get('PATH_INFO', ''), 1, 2, True) - if rest and rest.startswith(self.reseller_prefix): - # annoynomous access to my reseller's accounts - env['swift.authorize'] = self.authorize - env['swift.clean_acl'] = clean_acl - else: - # not my token, not my account - # good idea regardless... - if 'swift.authorize' not in env: - env['swift.authorize'] = self.denied_response - - return self.app(env, start_response) + set_memcache(expiration, groups) + return groups def authorize(self, req): """ @@ -153,6 +169,7 @@ def filter_factory(global_conf, **local_conf): """Returns a WSGI filter app for use with paste.deploy.""" conf = global_conf.copy() conf.update(local_conf) + def auth_filter(app): return DevAuth(app, conf) return auth_filter diff --git a/test/unit/common/middleware/test_auth.py b/test/unit/common/middleware/test_auth.py index 1f5b0aaffc..a7ec9199a2 100644 --- a/test/unit/common/middleware/test_auth.py +++ b/test/unit/common/middleware/test_auth.py @@ -164,6 +164,26 @@ class TestAuth(unittest.TestCase): finally: auth.http_connect = old_http_connect + def test_auth_no_reseller_prefix_allow(self): + # Ensures that when we have no reseller prefix, we can still allow + # access if our auth server accepts requests + old_http_connect = auth.http_connect + try: + local_app = FakeApp() + local_auth = \ + auth.filter_factory({'reseller_prefix': ''})(local_app) + auth.http_connect = mock_http_connect(204, + {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'}) + reqenv = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/act', + 'HTTP_X_AUTH_TOKEN': 't', 'swift.cache': None} + result = ''.join(local_auth(reqenv, lambda x, y: None)) + self.assert_(result.startswith('204'), result) + self.assert_(local_app.i_was_called) + self.assertEquals(reqenv['swift.authorize'], + local_auth.authorize) + finally: + auth.http_connect = old_http_connect + def test_auth_no_reseller_prefix_no_token(self): # Check that normally we set up a call back to our authorize. local_auth = \ From c5513de8ac59ae5d55f7d416d0edf8a9b49f439a Mon Sep 17 00:00:00 2001 From: gholt Date: Fri, 17 Sep 2010 11:26:30 -0700 Subject: [PATCH 5/5] More doc updates, little refactoring too --- swift/common/middleware/auth.py | 92 +++++++++++++++++++-------------- swift/common/utils.py | 2 +- 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/swift/common/middleware/auth.py b/swift/common/middleware/auth.py index b191dbee6b..a4dd5ac7df 100644 --- a/swift/common/middleware/auth.py +++ b/swift/common/middleware/auth.py @@ -34,8 +34,7 @@ class DevAuth(object): self.reseller_prefix += '_' self.auth_host = conf.get('ip', '127.0.0.1') self.auth_port = int(conf.get('port', 11000)) - self.ssl = \ - conf.get('ssl', 'false').lower() in TRUE_VALUES + self.ssl = conf.get('ssl', 'false').lower() in TRUE_VALUES self.timeout = int(conf.get('node_timeout', 10)) def __call__(self, env, start_response): @@ -45,76 +44,87 @@ class DevAuth(object): validation. For an authenticated request, REMOTE_USER will be set to a comma separated list of the user's groups. - With out a reseller prefix, I act as the default auth service for - all requests, but I won't overwrite swift.authorize unless my auth - explictly grants groups to this token + With a non-empty reseller prefix, acts as the definitive auth service + for just tokens and accounts that begin with that prefix, but will deny + requests outside this prefix if no other auth middleware overrides it. - As a reseller, I must respect that my auth server is not - authorative for all tokens, but I can set swift.authorize to - denied_reponse if it's not already set + With an empty reseller prefix, acts as the definitive auth service only + for tokens that validate to a non-empty set of groups. For all other + requests, acts as the fallback auth service when no other auth + middleware overrides it. """ token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) if token and token.startswith(self.reseller_prefix): - # N.B. no reseller_prefix will match all tokens! - # attempt to auth my token with my auth server - groups = self.get_groups(token, - memcache_client=cache_from_env(env)) + # Note: Empty reseller_prefix will match all tokens. + # Attempt to auth my token with my auth server + groups = \ + self.get_groups(token, memcache_client=cache_from_env(env)) if groups: env['REMOTE_USER'] = groups user = groups and groups.split(',', 1)[0] or '' + # We know the proxy logs the token, so we augment it just a bit + # to also log the authenticated user. env['HTTP_X_AUTH_TOKEN'] = '%s,%s' % (user, token) env['swift.authorize'] = self.authorize env['swift.clean_acl'] = clean_acl else: - # unauthorized token + # Unauthorized token if self.reseller_prefix: - # because I own this token, I can deny it outright + # Because I know I'm the definitive auth for this token, I + # can deny it outright. return HTTPUnauthorized()(env, start_response) + # Because I'm not certain if I'm the definitive auth for empty + # reseller_prefixed tokens, I won't overwrite swift.authorize. elif 'swift.authorize' not in env: - # default auth won't over-write swift.authorize env['swift.authorize'] = self.denied_response else: if self.reseller_prefix: - # As a reseller, I would like to be calledback for annoynomous - # access to my accounts + # With a non-empty reseller_prefix, I would like to be called + # back for anonymous access to accounts I know I'm the + # definitive auth for. version, rest = split_path(env.get('PATH_INFO', ''), 1, 2, True) if rest and rest.startswith(self.reseller_prefix): - # handle annoynomous access to my reseller's accounts + # Handle anonymous access to accounts I'm the definitive + # auth for. env['swift.authorize'] = self.authorize env['swift.clean_acl'] = clean_acl + # Not my token, not my account, I can't authorize this request, + # deny all is a good idea if not already set... elif 'swift.authorize' not in env: - # not my token, not my account, I can't authorize this - # request, deny all is a good idea if not already set... env['swift.authorize'] = self.denied_response + # Because I'm not certain if I'm the definitive auth for empty + # reseller_prefixed accounts, I won't overwrite swift.authorize. elif 'swift.authorize' not in env: - # As a default auth, I'm willing to handle annoynomous requests - # for all accounts, but I won't over-write swift.authorize env['swift.authorize'] = self.authorize env['swift.clean_acl'] = clean_acl - return self.app(env, start_response) def get_groups(self, token, memcache_client=None): """ - Get groups for the given token, may use a memcache_client if set, and - update cache/expire old values; otherwise call auth_host and return - 'x-auth-groups' + Get groups for the given token. + + If memcache_client is set, token credentials will be cached + appropriately. + + With a cache miss, or no memcache_client, the configurated external + authentication server will be queried for the group information. + + :param token: Token to validate and return a group string for. + :param memcache_client: Memcached client to use for caching token + credentials; None if no caching is desired. + :returns: None if the token is invalid or a string containing a comma + separated list of groups the authenticated user is a member + of. The first group in the list is also considered a unique + identifier for that user. """ groups = None - if memcache_client: - key = '%s/token/%s' % (self.reseller_prefix, token) - cached_auth_data = memcache_client.get(key) - if cached_auth_data: - start, expiration, groups = cached_auth_data - if time() - start > expiration: - groups = None - - def set_memcache(expiration, groups, key=key): - memcache_client.set(key, (time(), expiration, groups), - timeout=expiration) - else: - set_memcache = lambda *args: None + key = '%s/token/%s' % (self.reseller_prefix, token) + cached_auth_data = memcache_client and memcache_client.get(key) + if cached_auth_data: + start, expiration, groups = cached_auth_data + if time() - start > expiration: + groups = None if not groups: with Timeout(self.timeout): conn = http_connect(self.auth_host, self.auth_port, 'GET', @@ -126,7 +136,9 @@ class DevAuth(object): return None expiration = float(resp.getheader('x-auth-ttl')) groups = resp.getheader('x-auth-groups') - set_memcache(expiration, groups) + if memcache_client: + memcache_client.set(key, (time(), expiration, groups), + timeout=expiration) return groups def authorize(self, req): diff --git a/swift/common/utils.py b/swift/common/utils.py index a6a42a4870..f2e186c03e 100644 --- a/swift/common/utils.py +++ b/swift/common/utils.py @@ -56,7 +56,7 @@ _posix_fadvise = None HASH_PATH_SUFFIX = os.environ.get('SWIFT_HASH_PATH_SUFFIX', 'endcap') # Used when reading config values -TRUE_VALUES = set(('true', '1', 'yes', 'True', 'Yes')) +TRUE_VALUES = set(('true', '1', 'yes', 'True', 'Yes', 'on', 'On')) def load_libc_function(func_name):