diff --git a/swauth/middleware.py b/swauth/middleware.py index 91026a0..10b7bec 100644 --- a/swauth/middleware.py +++ b/swauth/middleware.py @@ -38,6 +38,25 @@ from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed from swift.common.utils import cache_from_env, get_logger, split_path, urlparse +# NOTE: This should be removed after some time when anyone upgrading Swauth +# should have also upgraded Swift. +try: + # Attempt to import get_remote_client; older versions of Swift don't have + # this. + from swift.common.utils import get_remote_client +except ImportError: + # Fall back to locally defined version. + def get_remote_client(req): + # remote host for zeus + client = req.headers.get('x-cluster-client-ip') + if not client and 'x-forwarded-for' in req.headers: + # remote host for other lbs + client = req.headers['x-forwarded-for'].split(',')[0].strip() + if not client: + client = req.remote_addr + return client + + class Swauth(object): """ Scalable authentication and authorization system that uses Swift as its @@ -101,6 +120,9 @@ class Swauth(object): self.timeout = int(conf.get('node_timeout', 10)) self.itoken = None self.itoken_expires = None + self.allowed_sync_hosts = [h.strip() + for h in conf.get('allowed_sync_hosts', '127.0.0.1').split(',') + if h.strip()] def __call__(self, env, start_response): """ @@ -269,11 +291,20 @@ class Swauth(object): if '.reseller_admin' in user_groups and \ account != self.reseller_prefix and \ account[len(self.reseller_prefix)] != '.': + req.environ['swift_owner'] = True return None if account in user_groups and \ (req.method not in ('DELETE', 'PUT') or container): # If the user is admin for the account and is not trying to do an # account DELETE or PUT... + req.environ['swift_owner'] = True + return None + if (req.environ.get('swift_sync_key') and + req.environ['swift_sync_key'] == + req.headers.get('x-container-sync-key', None) and + 'x-timestamp' in req.headers and + (req.remote_addr in self.allowed_sync_hosts or + get_remote_client(req) in self.allowed_sync_hosts)): return None referrers, groups = parse_acl(getattr(req, 'acl', None)) if referrer_allowed(req.referer, referrers): diff --git a/test_swauth/unit/test_middleware.py b/test_swauth/unit/test_middleware.py index a38bc5b..04ddcef 100644 --- a/test_swauth/unit/test_middleware.py +++ b/test_swauth/unit/test_middleware.py @@ -56,15 +56,21 @@ class FakeMemcache(object): class FakeApp(object): - def __init__(self, status_headers_body_iter=None): + def __init__(self, status_headers_body_iter=None, acl=None, sync_key=None): self.calls = 0 self.status_headers_body_iter = status_headers_body_iter if not self.status_headers_body_iter: self.status_headers_body_iter = iter([('404 Not Found', {}, '')]) + self.acl = acl + self.sync_key = sync_key def __call__(self, env, start_response): self.calls += 1 self.request = Request.blank('', environ=env) + if self.acl: + self.request.acl = self.acl + if self.sync_key: + self.request.environ['swift_sync_key'] = self.sync_key if 'swift.authorize' in env: resp = env['swift.authorize'](self.request) if resp: @@ -3260,6 +3266,173 @@ class TestAuth(unittest.TestCase): self._get_token_success_v1_0_encoded( 'act:u s r', 'k:e:y', 'act%3au%20s%20r', 'k%3Ae%3ay') + def test_allowed_sync_hosts(self): + a = auth.filter_factory({'super_admin_key': 'supertest'})(FakeApp()) + self.assertEquals(a.allowed_sync_hosts, ['127.0.0.1']) + a = auth.filter_factory({'super_admin_key': 'supertest', + 'allowed_sync_hosts': + '1.1.1.1,2.1.1.1, 3.1.1.1 , 4.1.1.1,, , 5.1.1.1'})(FakeApp()) + self.assertEquals(a.allowed_sync_hosts, + ['1.1.1.1', '2.1.1.1', '3.1.1.1', '4.1.1.1', '5.1.1.1']) + + def test_reseller_admin_is_owner(self): + orig_authorize = self.test_auth.authorize + owner_values = [] + + def mitm_authorize(req): + rv = orig_authorize(req) + owner_values.append(req.environ.get('swift_owner', False)) + return rv + + self.test_auth.authorize = mitm_authorize + + self.test_auth.app = FakeApp(iter([ + ('200 Ok', {}, + json.dumps({'account': 'other', 'user': 'other:usr', + 'account_id': 'AUTH_other', + 'groups': [{'name': 'other:usr'}, {'name': 'other'}, + {'name': '.reseller_admin'}], + 'expires': time() + 60})), + ('204 No Content', {}, '')])) + req = Request.blank('/v1/AUTH_cfa', headers={'X-Auth-Token': 'AUTH_t'}) + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, 204) + self.assertEquals(owner_values, [True]) + + def test_admin_is_owner(self): + orig_authorize = self.test_auth.authorize + owner_values = [] + + def mitm_authorize(req): + rv = orig_authorize(req) + owner_values.append(req.environ.get('swift_owner', False)) + return rv + + self.test_auth.authorize = mitm_authorize + + self.test_auth.app = FakeApp(iter([ + ('200 Ok', {}, + json.dumps({'account': 'act', 'user': 'act:usr', + 'account_id': 'AUTH_cfa', + 'groups': [{'name': 'act:usr'}, {'name': 'act'}, + {'name': '.admin'}], + 'expires': time() + 60})), + ('204 No Content', {}, '')])) + req = Request.blank('/v1/AUTH_cfa', headers={'X-Auth-Token': 'AUTH_t'}) + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, 204) + self.assertEquals(owner_values, [True]) + + def test_regular_is_not_owner(self): + orig_authorize = self.test_auth.authorize + owner_values = [] + + def mitm_authorize(req): + rv = orig_authorize(req) + owner_values.append(req.environ.get('swift_owner', False)) + return rv + + self.test_auth.authorize = mitm_authorize + + self.test_auth.app = FakeApp(iter([ + ('200 Ok', {}, + json.dumps({'account': 'act', 'user': 'act:usr', + 'account_id': 'AUTH_cfa', + 'groups': [{'name': 'act:usr'}, {'name': 'act'}], + 'expires': time() + 60})), + ('204 No Content', {}, '')]), acl='act:usr') + req = Request.blank('/v1/AUTH_cfa/c', + headers={'X-Auth-Token': 'AUTH_t'}) + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, 204) + self.assertEquals(owner_values, [False]) + + def test_sync_request_success(self): + self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), + sync_key='secret') + req = Request.blank('/v1/AUTH_cfa/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'x-container-sync-key': 'secret', + 'x-timestamp': '123.456'}) + req.remote_addr = '127.0.0.1' + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, 204) + + def test_sync_request_fail_key(self): + self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), + sync_key='secret') + req = Request.blank('/v1/AUTH_cfa/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'x-container-sync-key': 'wrongsecret', + 'x-timestamp': '123.456'}) + req.remote_addr = '127.0.0.1' + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, 401) + + self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), + sync_key='othersecret') + req = Request.blank('/v1/AUTH_cfa/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'x-container-sync-key': 'secret', + 'x-timestamp': '123.456'}) + req.remote_addr = '127.0.0.1' + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, 401) + + self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), + sync_key=None) + req = Request.blank('/v1/AUTH_cfa/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'x-container-sync-key': 'secret', + 'x-timestamp': '123.456'}) + req.remote_addr = '127.0.0.1' + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, 401) + + def test_sync_request_fail_no_timestamp(self): + self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), + sync_key='secret') + req = Request.blank('/v1/AUTH_cfa/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'x-container-sync-key': 'secret'}) + req.remote_addr = '127.0.0.1' + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, 401) + + def test_sync_request_fail_sync_host(self): + self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), + sync_key='secret') + req = Request.blank('/v1/AUTH_cfa/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'x-container-sync-key': 'secret', + 'x-timestamp': '123.456'}) + req.remote_addr = '127.0.0.2' + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, 401) + + def test_sync_request_success_lb_sync_host(self): + self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), + sync_key='secret') + req = Request.blank('/v1/AUTH_cfa/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'x-container-sync-key': 'secret', + 'x-timestamp': '123.456', + 'x-forwarded-for': '127.0.0.1'}) + req.remote_addr = '127.0.0.2' + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, 204) + + self.test_auth.app = FakeApp(iter([('204 No Content', {}, '')]), + sync_key='secret') + req = Request.blank('/v1/AUTH_cfa/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'x-container-sync-key': 'secret', + 'x-timestamp': '123.456', + 'x-cluster-client-ip': '127.0.0.1'}) + req.remote_addr = '127.0.0.2' + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, 204) + if __name__ == '__main__': unittest.main()