From a5df15005b38879d85ef6806f1273e19fe84f25a Mon Sep 17 00:00:00 2001 From: gholt Date: Thu, 9 Sep 2010 10:24:25 -0700 Subject: [PATCH] Got rid of inter-reseller ACLs. Enforce ACLs to only work within a reseller space. Updated docs and tests. We can expand to inter-reseller in the future with ACLs like .x:RESELLER_group --- doc/source/development_auth.rst | 22 ++++--- etc/proxy-server.conf-sample | 10 +-- swift/auth/server.py | 4 +- swift/common/middleware/auth.py | 4 +- test/functional/sample.conf | 1 - test/functionalnosetests/swift_testing.py | 6 -- test/functionalnosetests/test_container.py | 21 +++--- test/unit/common/middleware/test_auth.py | 75 ++++++++++++---------- 8 files changed, 71 insertions(+), 72 deletions(-) diff --git a/doc/source/development_auth.rst b/doc/source/development_auth.rst index 51741dcca5..bccb6e7df5 100644 --- a/doc/source/development_auth.rst +++ b/doc/source/development_auth.rst @@ -35,14 +35,15 @@ string. If the user does not have admin access to the account, the third group will be omitted. It is highly recommended that authentication server implementers prefix their -group names and tokens with a configurable reseller prefix (`AUTH_` by default -with the included DevAuth). This prefix will allow deconflicting with other -authentication servers that might be using the same Swift cluster. +tokens and Swift storage accounts they create with a configurable reseller +prefix (`AUTH_` by default with the included DevAuth). This prefix will allow +deconflicting with other authentication servers that might be using the same +Swift cluster. Otherwise, the Swift cluster will have to try all the resellers +until one validates a token or all fail. -The only other restriction is that no group name should begin with a period '.' -as that is reserved for internal Swift use (such as the .r for referrer -designations as you'll see later). This shouldn't be an issue if a reseller -prefix is in use and does not begin with a period. +A restriction with group names is that no group name should begin with a period +'.' as that is reserved for internal Swift use (such as the .r for referrer +designations as you'll see later). Example Authentication with DevAuth: @@ -54,10 +55,11 @@ Example Authentication with DevAuth: it matches the "tester" user within the "test" account for the storage account "AUTH_storage_xyz". * The external DevAuth server responds with "X-Auth-Groups: - AUTH_test:tester,AUTH_test,AUTH_storage_xyz" + 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 anything with - ACLs specifying at least one of those three groups returned. + 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. 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/etc/proxy-server.conf-sample b/etc/proxy-server.conf-sample index 6ee36451ab..c3766bfd5d 100644 --- a/etc/proxy-server.conf-sample +++ b/etc/proxy-server.conf-sample @@ -37,10 +37,12 @@ use = egg:swift#proxy [filter:auth] use = egg:swift#auth -# The reseller prefix, if set, will verify a token begins with this prefix -# before even attempting to validate it with the external reseller. Usefull if -# multiple auth systems are in use for one Swift cluster. -# reseller_prefix = +# The reseller prefix will verify a token begins with this prefix before even +# attempting to validate it with the external authentication server. Also, with +# authorization, only Swift storage accounts with this prefix will be +# authorized by this middleware. Useful if multiple auth systems are in use for +# one Swift cluster. +# reseller_prefix = AUTH # ip = 127.0.0.1 # port = 11000 # ssl = false diff --git a/swift/auth/server.py b/swift/auth/server.py index 931bbb6080..38c72114e5 100644 --- a/swift/auth/server.py +++ b/swift/auth/server.py @@ -361,9 +361,7 @@ class AuthController(object): validation = self.validate_token(token) if not validation: return HTTPNotFound() - groups = [ - '%s%s:%s' % (self.reseller_prefix, validation[1], validation[2]), - '%s%s' % (self.reseller_prefix, validation[1])] + groups = ['%s:%s' % (validation[1], validation[2]), validation[1]] if validation[3]: # admin access to a cfaccount groups.append(validation[3]) return HTTPNoContent(headers={'X-Auth-TTL': validation[0], diff --git a/swift/common/middleware/auth.py b/swift/common/middleware/auth.py index d26df3e44a..03770175fd 100644 --- a/swift/common/middleware/auth.py +++ b/swift/common/middleware/auth.py @@ -29,7 +29,7 @@ class DevAuth(object): def __init__(self, app, conf): self.app = app self.conf = conf - self.reseller_prefix = conf.get('reseller_prefix', '').strip() + self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip() if self.reseller_prefix and self.reseller_prefix[-1] != '_': self.reseller_prefix += '_' self.auth_host = conf.get('ip', '127.0.0.1') @@ -83,7 +83,7 @@ class DevAuth(object): WSGI response callable if not. """ version, account, container, obj = split_path(req.path, 1, 4, True) - if not account: + if not account or not account.startswith(self.reseller_prefix): return self.denied_response(req) if req.remote_user and account in req.remote_user.split(','): return None diff --git a/test/functional/sample.conf b/test/functional/sample.conf index 646541e905..983f2cf768 100644 --- a/test/functional/sample.conf +++ b/test/functional/sample.conf @@ -2,7 +2,6 @@ auth_host = 127.0.0.1 auth_port = 11000 auth_ssl = no -auth_prefix = AUTH # Primary functional test account (needs admin access to the account) account = test diff --git a/test/functionalnosetests/swift_testing.py b/test/functionalnosetests/swift_testing.py index 974879cc09..8bd46b462b 100644 --- a/test/functionalnosetests/swift_testing.py +++ b/test/functionalnosetests/swift_testing.py @@ -10,9 +10,6 @@ from swift.common.client import get_auth, http_connection swift_test_auth = os.environ.get('SWIFT_TEST_AUTH') -swift_test_auth_prefix = os.environ.get('SWIFT_TEST_AUTH_PREFIX') -if swift_test_auth_prefix and swift_test_auth_prefix[-1] != '_': - swift_test_auth_prefix += '_' swift_test_user = [os.environ.get('SWIFT_TEST_USER'), None, None] swift_test_key = [os.environ.get('SWIFT_TEST_KEY'), None, None] @@ -35,9 +32,6 @@ if not all([swift_test_auth, swift_test_user[0], swift_test_key[0]]): if conf.get('auth_ssl', 'no').lower() in ('yes', 'true', 'on', '1'): swift_test_auth = 'https' swift_test_auth += '://%(auth_host)s:%(auth_port)s/v1.0' % conf - swift_test_auth_prefix = conf.get('auth_prefix', 'AUTH') - if swift_test_auth_prefix and swift_test_auth_prefix[-1] != '_': - swift_test_auth_prefix += '_' swift_test_user[0] = '%(account)s:%(username)s' % conf swift_test_key[0] = conf['password'] try: diff --git a/test/functionalnosetests/test_container.py b/test/functionalnosetests/test_container.py index c1dca1cb6b..96c0be91e6 100755 --- a/test/functionalnosetests/test_container.py +++ b/test/functionalnosetests/test_container.py @@ -9,7 +9,7 @@ from swift.common.constraints import MAX_META_COUNT, MAX_META_NAME_LENGTH, \ MAX_META_OVERALL_SIZE, MAX_META_VALUE_LENGTH from swift_testing import check_response, retry, skip, skip2, skip3, \ - swift_test_auth_prefix, swift_test_user + swift_test_user class TestContainer(unittest.TestCase): @@ -377,10 +377,9 @@ class TestContainer(unittest.TestCase): self.assertEquals(resp.status, 403) # Make the container accessible by the second account def post(url, token, parsed, conn): - conn.request('POST', parsed.path + '/' + self.name, '', { - 'X-Auth-Token': token, - 'X-Container-Read': '%stest2' % swift_test_auth_prefix, - 'X-Container-Write': '%stest2' % swift_test_auth_prefix}) + conn.request('POST', parsed.path + '/' + self.name, '', + {'X-Auth-Token': token, 'X-Container-Read': 'test2', + 'X-Container-Write': 'test2'}) return check_response(conn) resp = retry(post) resp.read() @@ -446,9 +445,8 @@ class TestContainer(unittest.TestCase): self.assertEquals(resp.status, 403) # Now make the container also writeable by the second account def post(url, token, parsed, conn): - conn.request('POST', parsed.path + '/' + self.name, '', { - 'X-Auth-Token': token, - 'X-Container-Write': '%stest2' % swift_test_auth_prefix}) + conn.request('POST', parsed.path + '/' + self.name, '', + {'X-Auth-Token': token, 'X-Container-Write': 'test2'}) return check_response(conn) resp = retry(post) resp.read() @@ -485,9 +483,7 @@ class TestContainer(unittest.TestCase): # Make the container accessible by the third account def post(url, token, parsed, conn): conn.request('POST', parsed.path + '/' + self.name, '', - {'X-Auth-Token': token, - 'X-Container-Read': '%s%s' % - (swift_test_auth_prefix, swift_test_user[2])}) + {'X-Auth-Token': token, 'X-Container-Read': swift_test_user[2]}) return check_response(conn) resp = retry(post) resp.read() @@ -508,8 +504,7 @@ class TestContainer(unittest.TestCase): def post(url, token, parsed, conn): conn.request('POST', parsed.path + '/' + self.name, '', {'X-Auth-Token': token, - 'X-Container-Write': '%s%s' % - (swift_test_auth_prefix, swift_test_user[2])}) + 'X-Container-Write': swift_test_user[2]}) return check_response(conn) resp = retry(post) resp.read() diff --git a/test/unit/common/middleware/test_auth.py b/test/unit/common/middleware/test_auth.py index 366492f09b..e19d6d4ca9 100644 --- a/test/unit/common/middleware/test_auth.py +++ b/test/unit/common/middleware/test_auth.py @@ -109,7 +109,7 @@ class TestAuth(unittest.TestCase): try: auth.http_connect = mock_http_connect(404) result = ''.join(self.test_auth({'REQUEST_METHOD': 'GET', - 'HTTP_X_AUTH_TOKEN': 't', 'swift.cache': FakeMemcache()}, + 'HTTP_X_AUTH_TOKEN': 'AUTH_t', 'swift.cache': FakeMemcache()}, lambda x, y: None)) self.assert_(result.startswith('401'), result) finally: @@ -119,9 +119,9 @@ class TestAuth(unittest.TestCase): old_http_connect = auth.http_connect try: auth.http_connect = mock_http_connect(204, - {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,cfa'}) + {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'}) result = ''.join(self.test_auth({'REQUEST_METHOD': 'GET', - 'HTTP_X_AUTH_TOKEN': 't', 'swift.cache': FakeMemcache()}, + 'HTTP_X_AUTH_TOKEN': 'AUTH_t', 'swift.cache': FakeMemcache()}, lambda x, y: None)) self.assert_(result.startswith('204'), result) finally: @@ -132,15 +132,15 @@ class TestAuth(unittest.TestCase): try: fake_memcache = FakeMemcache() auth.http_connect = mock_http_connect(204, - {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,cfa'}) + {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'}) result = ''.join(self.test_auth({'REQUEST_METHOD': 'GET', - 'HTTP_X_AUTH_TOKEN': 't', 'swift.cache': fake_memcache}, + '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': 't', 'swift.cache': fake_memcache}, + 'HTTP_X_AUTH_TOKEN': 'AUTH_t', 'swift.cache': fake_memcache}, lambda x, y: None)) self.assert_(result.startswith('204'), result) finally: @@ -151,15 +151,15 @@ class TestAuth(unittest.TestCase): try: fake_memcache = FakeMemcache() auth.http_connect = mock_http_connect(204, - {'x-auth-ttl': '0', 'x-auth-groups': 'act:usr,act,cfa'}) + {'x-auth-ttl': '0', 'x-auth-groups': 'act:usr,act,AUTH_cfa'}) result = ''.join(self.test_auth({'REQUEST_METHOD': 'GET', - 'HTTP_X_AUTH_TOKEN': 't', 'swift.cache': fake_memcache}, + '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 result = ''.join(self.test_auth({'REQUEST_METHOD': 'GET', - 'HTTP_X_AUTH_TOKEN': 't', 'swift.cache': fake_memcache}, + 'HTTP_X_AUTH_TOKEN': 'AUTH_t', 'swift.cache': fake_memcache}, lambda x, y: None)) self.assert_(result.startswith('401'), result) finally: @@ -169,12 +169,12 @@ class TestAuth(unittest.TestCase): old_http_connect = auth.http_connect try: auth.http_connect = mock_http_connect(204, - {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,cfa'}) - req = Request.blank('/v/a/c/o', headers={'x-auth-token': 't'}) + {'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.environ['swift.cache'] = FakeMemcache() result = ''.join(self.test_auth(req.environ, start_response)) self.assert_(result.startswith('204'), result) - self.assertEquals(req.remote_user, 'act:usr,act,cfa') + self.assertEquals(req.remote_user, 'act:usr,act,AUTH_cfa') finally: auth.http_connect = old_http_connect @@ -182,7 +182,7 @@ class TestAuth(unittest.TestCase): old_http_connect = auth.http_connect try: auth.http_connect = mock_http_connect(204, - {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,cfa'}) + {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'}) req = Request.blank('/v/a/c/o') req.environ['swift.cache'] = FakeMemcache() result = ''.join(self.test_auth(req.environ, start_response)) @@ -195,12 +195,13 @@ class TestAuth(unittest.TestCase): old_http_connect = auth.http_connect try: auth.http_connect = mock_http_connect(204, - {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,cfa'}) - req = Request.blank('/v/a/c/o', headers={'x-storage-token': 't'}) + {'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'}) + req = Request.blank('/v/a/c/o', + headers={'x-storage-token': 'AUTH_t'}) req.environ['swift.cache'] = FakeMemcache() result = ''.join(self.test_auth(req.environ, start_response)) self.assert_(result.startswith('204'), result) - self.assertEquals(req.remote_user, 'act:usr,act,cfa') + self.assertEquals(req.remote_user, 'act:usr,act,AUTH_cfa') finally: auth.http_connect = old_http_connect @@ -209,73 +210,81 @@ class TestAuth(unittest.TestCase): resp = str(self.test_auth.authorize(req)) self.assert_(resp.startswith('401'), resp) req = Request.blank('/badpath') - req.remote_user = 'act:usr,act,cfa' + req.remote_user = 'act:usr,act,AUTH_cfa' resp = str(self.test_auth.authorize(req)) self.assert_(resp.startswith('403'), resp) def test_authorize_account_access(self): - req = Request.blank('/v1/cfa') - req.remote_user = 'act:usr,act,cfa' + req = Request.blank('/v1/AUTH_cfa') + req.remote_user = 'act:usr,act,AUTH_cfa' self.assertEquals(self.test_auth.authorize(req), None) - req = Request.blank('/v1/cfa') + req = Request.blank('/v1/AUTH_cfa') req.remote_user = 'act:usr,act' resp = str(self.test_auth.authorize(req)) self.assert_(resp.startswith('403'), resp) def test_authorize_acl_group_access(self): - req = Request.blank('/v1/cfa') + req = Request.blank('/v1/AUTH_cfa') req.remote_user = 'act:usr,act' resp = str(self.test_auth.authorize(req)) self.assert_(resp.startswith('403'), resp) - req = Request.blank('/v1/cfa') + req = Request.blank('/v1/AUTH_cfa') req.remote_user = 'act:usr,act' req.acl = 'act' self.assertEquals(self.test_auth.authorize(req), None) - req = Request.blank('/v1/cfa') + req = Request.blank('/v1/AUTH_cfa') req.remote_user = 'act:usr,act' req.acl = 'act:usr' self.assertEquals(self.test_auth.authorize(req), None) - req = Request.blank('/v1/cfa') + req = Request.blank('/v1/AUTH_cfa') req.remote_user = 'act:usr,act' req.acl = 'act2' resp = str(self.test_auth.authorize(req)) self.assert_(resp.startswith('403'), resp) - req = Request.blank('/v1/cfa') + req = Request.blank('/v1/AUTH_cfa') req.remote_user = 'act:usr,act' req.acl = 'act:usr2' resp = str(self.test_auth.authorize(req)) self.assert_(resp.startswith('403'), resp) + def test_deny_cross_reseller(self): + # Tests that cross-reseller is denied, even if ACLs/group names match + req = Request.blank('/v1/OTHER_cfa') + req.remote_user = 'act:usr,act,AUTH_cfa' + req.acl = 'act' + resp = str(self.test_auth.authorize(req)) + self.assert_(resp.startswith('403'), resp) + def test_authorize_acl_referrer_access(self): - req = Request.blank('/v1/cfa') + req = Request.blank('/v1/AUTH_cfa') req.remote_user = 'act:usr,act' resp = str(self.test_auth.authorize(req)) self.assert_(resp.startswith('403'), resp) - req = Request.blank('/v1/cfa') + req = Request.blank('/v1/AUTH_cfa') req.remote_user = 'act:usr,act' req.acl = '.r:*' self.assertEquals(self.test_auth.authorize(req), None) - req = Request.blank('/v1/cfa') + req = Request.blank('/v1/AUTH_cfa') req.remote_user = 'act:usr,act' req.acl = '.r:.example.com' resp = str(self.test_auth.authorize(req)) self.assert_(resp.startswith('403'), resp) - req = Request.blank('/v1/cfa') + req = Request.blank('/v1/AUTH_cfa') req.remote_user = 'act:usr,act' req.referer = 'http://www.example.com/index.html' req.acl = '.r:.example.com' self.assertEquals(self.test_auth.authorize(req), None) - req = Request.blank('/v1/cfa') + req = Request.blank('/v1/AUTH_cfa') resp = str(self.test_auth.authorize(req)) self.assert_(resp.startswith('401'), resp) - req = Request.blank('/v1/cfa') + req = Request.blank('/v1/AUTH_cfa') req.acl = '.r:*' self.assertEquals(self.test_auth.authorize(req), None) - req = Request.blank('/v1/cfa') + req = Request.blank('/v1/AUTH_cfa') req.acl = '.r:.example.com' resp = str(self.test_auth.authorize(req)) self.assert_(resp.startswith('401'), resp) - req = Request.blank('/v1/cfa') + req = Request.blank('/v1/AUTH_cfa') req.referer = 'http://www.example.com/index.html' req.acl = '.r:.example.com' self.assertEquals(self.test_auth.authorize(req), None)