Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Account quotas

Add a new middleware implementing account quotas.

This middleware blocks write requests (PUT, POST) if a given quota (in bytes)
is exceeded while DELETE requests are still allowed.

Quotas are stored in the x-account-meta-quota-bytes metadata entry.
Write requests to this metadata setting are only allowed for resellers.

Change-Id: I57fd7c6209f34cc79d4bab72d500d43ba2a62083
  • Loading branch information...
commit 28c75db0e7103603e89e0a5ba3c32b7505e4d89e 1 parent 48380c5
@cschwede cschwede authored
View
8 doc/source/misc.rst
@@ -203,10 +203,16 @@ Static Large Objects
:members:
:show-inheritance:
-
List Endpoints
==============
.. automodule:: swift.common.middleware.list_endpoints
:members:
:show-inheritance:
+
+Account Quotas
+================
+
+.. automodule:: swift.common.middleware.account_quotas
+ :members:
+ :show-inheritance:
View
5 etc/proxy-server.conf-sample
@@ -34,7 +34,7 @@
# eventlet_debug = false
[pipeline:main]
-pipeline = catch_errors healthcheck proxy-logging cache slo ratelimit tempauth container-quotas proxy-logging proxy-server
+pipeline = catch_errors healthcheck proxy-logging cache slo ratelimit tempauth container-quotas account-quotas proxy-logging proxy-server
[app:proxy-server]
use = egg:swift#proxy
@@ -363,3 +363,6 @@ use = egg:swift#slo
# max_manifest_segments = 1000
# max_manifest_size = 2097152
# min_segment_size = 1048576
+
+[filter:account-quotas]
+use = egg:swift#account_quotas
View
2  setup.py
@@ -104,6 +104,8 @@
'bulk=swift.common.middleware.bulk:filter_factory',
'container_quotas=swift.common.middleware.container_quotas:'
'filter_factory',
+ 'account_quotas=swift.common.middleware.account_quotas:'
+ 'filter_factory',
'proxy_logging=swift.common.middleware.proxy_logging:'
'filter_factory',
'slo=swift.common.middleware.slo:filter_factory',
View
83 swift/common/middleware/account_quotas.py
@@ -0,0 +1,83 @@
+# Copyright (c) 2013 OpenStack Foundation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+""" Account quota middleware for Openstack Swift Proxy """
+
+from swift.common.swob import HTTPForbidden, HTTPRequestEntityTooLarge, \
+ HTTPBadRequest, wsgify
+
+from swift.proxy.controllers.base import get_account_info
+
+
+class AccountQuotaMiddleware(object):
+ """
+ account_quotas is a middleware which blocks write requests (PUT, POST) if a
+ given quota (in bytes) is exceeded while DELETE requests are still allowed.
+
+ account_quotas uses the x-account-meta-quota-bytes metadata to store the
+ quota. Write requests to this metadata setting are only allowed for
+ resellers. There is no quota limit if x-account-meta-quota-bytes is not
+ set.
+
+ The following shows an example proxy-server.conf:
+
+ [pipeline:main]
+ pipeline = catch_errors cache tempauth account-quotas proxy-server
+
+ [filter:account-quotas]
+ use = egg:swift#account_quotas
+
+ """
+
+ def __init__(self, app, *args, **kwargs):
+ self.app = app
+
+ @wsgify
+ def __call__(self, request):
+
+ if request.method not in ("POST", "PUT"):
+ return self.app
+
+ try:
+ request.split_path(2, 4, rest_with_last=True)
+ except ValueError:
+ return self.app
+
+ new_quota = request.headers.get('X-Account-Meta-Quota-Bytes')
+
+ if request.environ.get('reseller_request') is True:
+ if new_quota and not new_quota.isdigit():
+ return HTTPBadRequest()
+ return self.app
+
+ # deny quota set for non-reseller
+ if new_quota is not None:
+ return HTTPForbidden()
+
+ account_info = get_account_info(request.environ, self.app)
+ new_size = int(account_info['bytes']) + (request.content_length or 0)
+ quota = int(account_info['meta'].get('quota-bytes', -1))
+
+ if 0 <= quota < new_size:
+ return HTTPRequestEntityTooLarge()
+
+ return self.app
+
+
+def filter_factory(global_conf, **local_conf):
+ """Returns a WSGI filter app for use with paste.deploy."""
+ def account_quota_filter(app):
+ return AccountQuotaMiddleware(app)
+ return account_quota_filter
View
2  swift/common/middleware/keystoneauth.py
@@ -106,6 +106,8 @@ def __call__(self, environ, start_response):
environ['keystone.identity'] = identity
environ['REMOTE_USER'] = identity.get('tenant')
environ['swift.authorize'] = self.authorize
+ if self.reseller_admin_role in identity.get('roles', []):
+ environ['reseller_request'] = True
else:
self.logger.debug('Authorizing as anonymous')
environ['swift.authorize'] = self.authorize_anonymous
View
2  swift/common/middleware/tempauth.py
@@ -150,6 +150,8 @@ def __call__(self, env, start_response):
'%s,%s' % (user, 's3' if s3 else token)
env['swift.authorize'] = self.authorize
env['swift.clean_acl'] = clean_acl
+ if '.reseller_admin' in groups:
+ env['reseller_request'] = True
else:
# Unauthorized token
if self.reseller_prefix:
View
171 test/unit/common/middleware/test_account_quotas.py
@@ -0,0 +1,171 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+from swift.common.swob import Request
+
+from swift.common.middleware import account_quotas
+
+
+class FakeCache(object):
+ def __init__(self, val):
+ self.val = val
+
+ def get(self, *args):
+ return self.val
+
+ def set(self, *args, **kwargs):
+ pass
+
+
+class FakeApp(object):
+ def __init__(self, headers=[]):
+ self.headers = headers
+
+ def __call__(self, env, start_response):
+ start_response('200 OK', self.headers)
+ return []
+
+
+def start_response(*args):
+ pass
+
+
+class TestAccountQuota(unittest.TestCase):
+
+ def test_unauthorized(self):
+ headers = [('x-account-bytes-used', '1000'), ]
+ app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
+ cache = FakeCache(None)
+ req = Request.blank('/v1/a/c/o',
+ environ={'REQUEST_METHOD': 'PUT',
+ 'swift.cache': cache})
+ res = req.get_response(app)
+ #Response code of 200 because authentication itself is not done here
+ self.assertEquals(res.status_int, 200)
+
+ def test_no_quotas(self):
+ headers = [('x-account-bytes-used', '1000'), ]
+ app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
+ cache = FakeCache(None)
+ req = Request.blank('/v1/a/c/o',
+ environ={'REQUEST_METHOD': 'PUT',
+ 'swift.cache': cache})
+ res = req.get_response(app)
+ self.assertEquals(res.status_int, 200)
+
+ def test_exceed_bytes_quota(self):
+ headers = [('x-account-bytes-used', '1000'),
+ ('x-account-meta-quota-bytes', '0')]
+ app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
+ cache = FakeCache(None)
+ req = Request.blank('/v1/a/c/o',
+ environ={'REQUEST_METHOD': 'PUT',
+ 'swift.cache': cache})
+ res = req.get_response(app)
+ self.assertEquals(res.status_int, 413)
+
+ def test_exceed_bytes_quota_reseller(self):
+ headers = [('x-account-bytes-used', '1000'),
+ ('x-account-meta-quota-bytes', '0')]
+ app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
+ cache = FakeCache(None)
+ req = Request.blank('/v1/a/c/o',
+ environ={'REQUEST_METHOD': 'PUT',
+ 'swift.cache': cache,
+ 'reseller_request': True})
+ res = req.get_response(app)
+ self.assertEquals(res.status_int, 200)
+
+ def test_not_exceed_bytes_quota(self):
+ headers = [('x-account-bytes-used', '1000'),
+ ('x-account-meta-quota-bytes', 2000)]
+ app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
+ cache = FakeCache(None)
+ req = Request.blank('/v1/a/c/o',
+ environ={'REQUEST_METHOD': 'PUT',
+ 'swift.cache': cache})
+ res = req.get_response(app)
+ self.assertEquals(res.status_int, 200)
+
+ def test_invalid_quotas(self):
+ headers = [('x-account-bytes-used', '0'), ]
+ app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
+ cache = FakeCache(None)
+ req = Request.blank('/v1/a/c',
+ environ={'REQUEST_METHOD': 'POST',
+ 'swift.cache': cache,
+ 'HTTP_X_ACCOUNT_META_QUOTA_BYTES': 'abc',
+ 'reseller_request': True})
+ res = req.get_response(app)
+ self.assertEquals(res.status_int, 400)
+
+ def test_valid_quotas_admin(self):
+ headers = [('x-account-bytes-used', '0'), ]
+ app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
+ cache = FakeCache(None)
+ req = Request.blank('/v1/a/c',
+ environ={'REQUEST_METHOD': 'POST',
+ 'swift.cache': cache,
+ 'HTTP_X_ACCOUNT_META_QUOTA_BYTES': '100'})
+ res = req.get_response(app)
+ self.assertEquals(res.status_int, 403)
+
+ def test_valid_quotas_reseller(self):
+ headers = [('x-account-bytes-used', '0'), ]
+ app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
+ cache = FakeCache(None)
+ req = Request.blank('/v1/a/c',
+ environ={'REQUEST_METHOD': 'POST',
+ 'swift.cache': cache,
+ 'HTTP_X_ACCOUNT_META_QUOTA_BYTES': '100',
+ 'reseller_request': True})
+ res = req.get_response(app)
+ self.assertEquals(res.status_int, 200)
+
+ def test_delete_quotas(self):
+ headers = [('x-account-bytes-used', '0'), ]
+ app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
+ cache = FakeCache(None)
+ req = Request.blank('/v1/a/c',
+ environ={'REQUEST_METHOD': 'POST',
+ 'swift.cache': cache,
+ 'HTTP_X_ACCOUNT_META_QUOTA_BYTES': ''})
+ res = req.get_response(app)
+ self.assertEquals(res.status_int, 403)
+
+ def test_delete_quotas_reseller(self):
+ headers = [('x-account-bytes-used', '0'), ]
+ app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
+ req = Request.blank('/v1/a/c',
+ environ={'REQUEST_METHOD': 'POST',
+ 'HTTP_X_ACCOUNT_META_QUOTA_BYTES': '',
+ 'reseller_request': True})
+ res = req.get_response(app)
+ self.assertEquals(res.status_int, 200)
+
+ def test_invalid_request_exception(self):
+ headers = [('x-account-bytes-used', '1000'), ]
+ app = account_quotas.AccountQuotaMiddleware(FakeApp(headers))
+ cache = FakeCache(None)
+ req = Request.blank('/v1',
+ environ={'REQUEST_METHOD': 'PUT',
+ 'swift.cache': cache})
+ res = req.get_response(app)
+ #Response code of 200 because authentication itself is not done here
+ self.assertEquals(res.status_int, 200)
+
+
+if __name__ == '__main__':
+ unittest.main()
Please sign in to comment.
Something went wrong with that request. Please try again.