diff --git a/doc/source/deployment_guide.rst b/doc/source/deployment_guide.rst index aa7eef2b6f..68f8c9b5c8 100644 --- a/doc/source/deployment_guide.rst +++ b/doc/source/deployment_guide.rst @@ -462,6 +462,8 @@ error_suppression_interval 60 Time in seconds that must no longer error limited error_suppression_limit 10 Error count to consider a node error limited +allow_account_management false Whether account PUTs and DELETEs + are even callable ============================ =============== ============================= [auth] diff --git a/doc/source/development_saio.rst b/doc/source/development_saio.rst index 7b9e0c780c..999d338fbf 100644 --- a/doc/source/development_saio.rst +++ b/doc/source/development_saio.rst @@ -241,6 +241,7 @@ Sample configuration files are provided with all defaults in line-by-line commen [app:proxy-server] use = egg:swift#proxy + allow_account_management = true [filter:auth] use = egg:swift#auth diff --git a/doc/source/howto_installmultinode.rst b/doc/source/howto_installmultinode.rst index 9f485e8df9..82a3a88099 100644 --- a/doc/source/howto_installmultinode.rst +++ b/doc/source/howto_installmultinode.rst @@ -124,6 +124,7 @@ Configure the Proxy node [app:proxy-server] use = egg:swift#proxy + allow_account_management = true [filter:auth] use = egg:swift#auth diff --git a/etc/proxy-server.conf-sample b/etc/proxy-server.conf-sample index 0c7477147c..220f003ba0 100644 --- a/etc/proxy-server.conf-sample +++ b/etc/proxy-server.conf-sample @@ -29,6 +29,9 @@ use = egg:swift#proxy # error_suppression_interval = 60 # How many errors can accumulate before a node is temporarily ignored. # error_suppression_limit = 10 +# If set to 'true' any authorized user may create and delete accounts; if +# 'false' no one, even authorized, can. +# allow_account_management = false [filter:auth] use = egg:swift#auth diff --git a/swift/proxy/server.py b/swift/proxy/server.py index 1be24f5cc7..e48052a398 100644 --- a/swift/proxy/server.py +++ b/swift/proxy/server.py @@ -1139,6 +1139,8 @@ class AccountController(Controller): @public def PUT(self, req): """HTTP PUT request handler.""" + if not self.app.allow_account_management: + return HTTPMethodNotAllowed(request=req) error_response = check_metadata(req, 'account') if error_response: return error_response @@ -1238,6 +1240,51 @@ class AccountController(Controller): return self.best_response(req, statuses, reasons, bodies, 'Account POST') + @public + def DELETE(self, req): + """HTTP DELETE request handler.""" + if not self.app.allow_account_management: + return HTTPMethodNotAllowed(request=req) + account_partition, accounts = \ + self.app.account_ring.get_nodes(self.account_name) + headers = {'X-Timestamp': normalize_timestamp(time.time()), + 'X-CF-Trans-Id': self.trans_id} + statuses = [] + reasons = [] + bodies = [] + for node in self.iter_nodes(account_partition, accounts, + self.app.account_ring): + if self.error_limited(node): + continue + try: + with ConnectionTimeout(self.app.conn_timeout): + conn = http_connect(node['ip'], node['port'], + node['device'], account_partition, 'DELETE', + req.path_info, headers) + with Timeout(self.app.node_timeout): + source = conn.getresponse() + body = source.read() + if 200 <= source.status < 300 \ + or 400 <= source.status < 500: + statuses.append(source.status) + reasons.append(source.reason) + bodies.append(body) + elif source.status == 507: + self.error_limit(node) + except: + self.exception_occurred(node, 'Account', + 'Trying to DELETE %s' % req.path) + if len(statuses) >= len(accounts): + break + while len(statuses) < len(accounts): + statuses.append(503) + reasons.append('') + bodies.append('') + if self.app.memcache: + self.app.memcache.delete('account%s' % req.path_info.rstrip('/')) + return self.best_response(req, statuses, reasons, bodies, + 'Account DELETE') + class BaseApplication(object): """Base WSGI application for the proxy server""" @@ -1265,6 +1312,8 @@ class BaseApplication(object): int(conf.get('recheck_container_existence', 60)) self.recheck_account_existence = \ int(conf.get('recheck_account_existence', 60)) + self.allow_account_management = \ + conf.get('allow_account_management', 'false').lower() == 'true' self.resellers_conf = ConfigParser() self.resellers_conf.read(os.path.join(swift_dir, 'resellers.conf')) self.object_ring = object_ring or \ diff --git a/test/unit/proxy/test_server.py b/test/unit/proxy/test_server.py index a2a9b3d778..11633dfcc7 100644 --- a/test/unit/proxy/test_server.py +++ b/test/unit/proxy/test_server.py @@ -2601,6 +2601,8 @@ class TestAccountController(unittest.TestCase): res = controller.PUT(req) expected = str(expected) self.assertEquals(res.status[:len(expected)], expected) + test_status_map((201, 201, 201), 405) + self.app.allow_account_management = True test_status_map((201, 201, 201), 201) test_status_map((201, 201, 500), 201) test_status_map((201, 500, 500), 503) @@ -2608,6 +2610,7 @@ class TestAccountController(unittest.TestCase): def test_PUT_max_account_name_length(self): with save_globals(): + self.app.allow_account_management = True controller = proxy_server.AccountController(self.app, '1' * 256) self.assert_status_map(controller.PUT, (201, 201, 201), 201) controller = proxy_server.AccountController(self.app, '2' * 257) @@ -2615,6 +2618,7 @@ class TestAccountController(unittest.TestCase): def test_PUT_connect_exceptions(self): with save_globals(): + self.app.allow_account_management = True controller = proxy_server.AccountController(self.app, 'account') self.assert_status_map(controller.PUT, (201, 201, -1), 201) self.assert_status_map(controller.PUT, (201, -1, -1), 503) @@ -2643,6 +2647,7 @@ class TestAccountController(unittest.TestCase): test_errors.append('%s: %s not in %s' % (test_header, test_value, headers)) with save_globals(): + self.app.allow_account_management = True controller = \ proxy_server.AccountController(self.app, 'a') proxy_server.http_connect = fake_http_connect(201, 201, 201, @@ -2661,6 +2666,7 @@ class TestAccountController(unittest.TestCase): def bad_metadata_helper(self, method): with save_globals(): + self.app.allow_account_management = True controller = proxy_server.AccountController(self.app, 'a') proxy_server.http_connect = fake_http_connect(200, 201, 201, 201) req = Request.blank('/a/c', environ={'REQUEST_METHOD': method}) @@ -2743,6 +2749,27 @@ class TestAccountController(unittest.TestCase): resp = getattr(controller, method)(req) self.assertEquals(resp.status_int, 400) + def test_DELETE(self): + with save_globals(): + controller = proxy_server.AccountController(self.app, 'account') + + def test_status_map(statuses, expected, **kwargs): + proxy_server.http_connect = \ + fake_http_connect(*statuses, **kwargs) + self.app.memcache.store = {} + req = Request.blank('/a', {'REQUEST_METHOD': 'DELETE'}) + req.content_length = 0 + self.app.update_request(req) + res = controller.DELETE(req) + expected = str(expected) + self.assertEquals(res.status[:len(expected)], expected) + test_status_map((201, 201, 201), 405) + self.app.allow_account_management = True + test_status_map((201, 201, 201), 201) + test_status_map((201, 201, 500), 201) + test_status_map((201, 500, 500), 503) + test_status_map((204, 500, 404), 503) + if __name__ == '__main__': unittest.main()