components/openstack/swift/patches/CVE-2014-7960.patch
changeset 3998 5bd484384122
parent 3997 0ca3f3d6c919
child 4002 95b8f35fcdd5
--- a/components/openstack/swift/patches/CVE-2014-7960.patch	Fri Mar 20 03:13:26 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,971 +0,0 @@
-This is a backport to Havana of the Icehouse backport of the fix to bug 1365350,
-plus a fix to the testsuite to actually test the problem (bug 1381159).  Because
-Havana is out of support, it is not a candidate for upstream submission,
-though a re-work of the testsuite changes to trunk Swift is.
-
-================================================================================
-
-From 2c4622a28ea04e1c6b2382189b0a1f6cccdc9c0f Mon Sep 17 00:00:00 2001
-From: "Richard (Rick) Hawkins" <[email protected]>
-Date: Wed, 1 Oct 2014 09:37:47 -0400
-Subject: [PATCH] Fix metadata overall limits bug
-
-Currently metadata limits are checked on a per request basis. If
-multiple requests are sent within the per request limits, it is
-possible to exceed the overall limits.  This patch adds an overall
-metadata check to ensure that multiple requests to add metadata to
-an account/container will check overall limits before adding
-the additional metadata.
-
-This is a backport to the stable/icehouse branch for commit SHA
-5b2c27a5874c2b5b0a333e4955b03544f6a8119f.
-
-Closes-Bug: 1365350
-
-Conflicts:
-	swift/common/db.py
-	swift/container/server.py
-
-Change-Id: Id9fca209c9c1216f1949de7108bbe332808f1045
----
- swift/account/server.py           |    4 +-
- swift/common/constraints.py       |    5 ++-
- swift/common/db.py                |   34 +++++++++++++-
- swift/container/server.py         |    4 +-
- test/functional/test_account.py   |   66 +++++++++++++++++++++++++++
- test/functional/test_container.py |   20 +++++++++
- test/unit/common/test_db.py       |   90 ++++++++++++++++++++++++++++++++++++-
- 7 files changed, 216 insertions(+), 7 deletions(-)
-
---- a/swift/account/server.py
-+++ b/swift/account/server.py
-@@ -156,7 +156,7 @@ class AccountController(object):
-                             for key, value in req.headers.iteritems()
-                             if key.lower().startswith('x-account-meta-'))
-             if metadata:
--                broker.update_metadata(metadata)
-+                broker.update_metadata(metadata, validate_metadata=True)
-             if created:
-                 return HTTPCreated(request=req)
-             else:
-@@ -262,7 +262,7 @@ class AccountController(object):
-                         for key, value in req.headers.iteritems()
-                         if key.lower().startswith('x-account-meta-'))
-         if metadata:
--            broker.update_metadata(metadata)
-+            broker.update_metadata(metadata, validate_metadata=True)
-         return HTTPNoContent(request=req)
- 
-     def __call__(self, env, start_response):
---- a/swift/common/constraints.py
-+++ b/swift/common/constraints.py
-@@ -68,7 +68,10 @@ FORMAT2CONTENT_TYPE = {'plain': 'text/pl
- 
- def check_metadata(req, target_type):
-     """
--    Check metadata sent in the request headers.
-+    Check metadata sent in the request headers.  This should only check
-+    that the metadata in the request given is valid.  Checks against
-+    account/container overall metadata should be forwarded on to its
-+    respective server to be checked.
- 
-     :param req: request object
-     :param target_type: str: one of: object, container, or account: indicates
---- a/swift/common/db.py
-+++ b/swift/common/db.py
-@@ -32,7 +32,9 @@ import sqlite3
- 
- from swift.common.utils import json, normalize_timestamp, renamer, \
-     mkdirs, lock_parent_directory, fallocate
-+from swift.common.constraints import MAX_META_COUNT, MAX_META_OVERALL_SIZE
- from swift.common.exceptions import LockTimeout
-+from swift.common.swob import HTTPBadRequest
- 
- 
- #: Whether calls will be made to preallocate disk space for database files.
-@@ -615,7 +617,35 @@ class DatabaseBroker(object):
-             metadata = {}
-         return metadata
- 
--    def update_metadata(self, metadata_updates):
-+    @staticmethod
-+    def validate_metadata(metadata):
-+        """
-+        Validates that metadata_falls within acceptable limits.
-+
-+        :param metadata: to be validated
-+        :raises: HTTPBadRequest if MAX_META_COUNT or MAX_META_OVERALL_SIZE
-+                 is exceeded
-+        """
-+        meta_count = 0
-+        meta_size = 0
-+        for key, (value, timestamp) in metadata.iteritems():
-+            key = key.lower()
-+            if value != '' and (key.startswith('x-account-meta') or
-+                                key.startswith('x-container-meta')):
-+                prefix = 'x-account-meta-'
-+                if key.startswith('x-container-meta-'):
-+                    prefix = 'x-container-meta-'
-+                key = key[len(prefix):]
-+                meta_count = meta_count + 1
-+                meta_size = meta_size + len(key) + len(value)
-+        if meta_count > MAX_META_COUNT:
-+            raise HTTPBadRequest('Too many metadata items; max %d'
-+                                 % MAX_META_COUNT)
-+        if meta_size > MAX_META_OVERALL_SIZE:
-+            raise HTTPBadRequest('Total metadata too large; max %d'
-+                                 % MAX_META_OVERALL_SIZE)
-+
-+    def update_metadata(self, metadata_updates, validate_metadata=False):
-         """
-         Updates the metadata dict for the database. The metadata dict values
-         are tuples of (value, timestamp) where the timestamp indicates when
-@@ -648,6 +678,8 @@ class DatabaseBroker(object):
-                 value, timestamp = value_timestamp
-                 if key not in md or timestamp > md[key][1]:
-                     md[key] = value_timestamp
-+            if validate_metadata:
-+                DatabaseBroker.validate_metadata(md)
-             conn.execute('UPDATE %s_stat SET metadata = ?' % self.db_type,
-                          (json.dumps(md),))
-             conn.commit()
---- a/swift/container/server.py
-+++ b/swift/container/server.py
-@@ -275,7 +275,7 @@ class ContainerController(object):
-                             metadata['X-Container-Sync-To'][0] != \
-                             broker.metadata['X-Container-Sync-To'][0]:
-                         broker.set_x_container_sync_points(-1, -1)
--                broker.update_metadata(metadata)
-+                broker.update_metadata(metadata, validate_metadata=True)
-             resp = self.account_update(req, account, container, broker)
-             if resp:
-                 return resp
-@@ -461,7 +461,7 @@ class ContainerController(object):
-                         metadata['X-Container-Sync-To'][0] != \
-                         broker.metadata['X-Container-Sync-To'][0]:
-                     broker.set_x_container_sync_points(-1, -1)
--            broker.update_metadata(metadata)
-+            broker.update_metadata(metadata, validate_metadata=True)
-         return HTTPNoContent(request=req)
- 
-     def __call__(self, env, start_response):
---- a/test/functional/swift_testing.py
-+++ b/test/functional/swift_testing.py
-@@ -0,0 +1,231 @@
-+# Copyright (c) 2010-2012 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.
-+
-+from httplib import HTTPException
-+import os
-+import socket
-+import sys
-+from time import sleep
-+from urlparse import urlparse
-+import functools
-+from nose import SkipTest
-+
-+from test import get_config
-+
-+from swiftclient import get_auth, http_connection
-+from test.functional.swift_test_client import Connection
-+
-+conf = get_config('func_test')
-+web_front_end = conf.get('web_front_end', 'integral')
-+normalized_urls = conf.get('normalized_urls', False)
-+
-+# If no conf was read, we will fall back to old school env vars
-+swift_test_auth = os.environ.get('SWIFT_TEST_AUTH')
-+swift_test_user = [os.environ.get('SWIFT_TEST_USER'), None, None]
-+swift_test_key = [os.environ.get('SWIFT_TEST_KEY'), None, None]
-+swift_test_tenant = ['', '', '']
-+swift_test_perm = ['', '', '']
-+
-+if conf:
-+    swift_test_auth_version = str(conf.get('auth_version', '1'))
-+
-+    swift_test_auth = 'http'
-+    if conf.get('auth_ssl', 'no').lower() in ('yes', 'true', 'on', '1'):
-+        swift_test_auth = 'https'
-+    if 'auth_prefix' not in conf:
-+        conf['auth_prefix'] = '/'
-+    try:
-+        suffix = '://%(auth_host)s:%(auth_port)s%(auth_prefix)s' % conf
-+        swift_test_auth += suffix
-+    except KeyError:
-+        pass  # skip
-+
-+    if swift_test_auth_version == "1":
-+        swift_test_auth += 'v1.0'
-+
-+        if 'account' in conf:
-+            swift_test_user[0] = '%(account)s:%(username)s' % conf
-+        else:
-+            swift_test_user[0] = '%(username)s' % conf
-+        swift_test_key[0] = conf['password']
-+        try:
-+            swift_test_user[1] = '%s%s' % (
-+                '%s:' % conf['account2'] if 'account2' in conf else '',
-+                conf['username2'])
-+            swift_test_key[1] = conf['password2']
-+        except KeyError as err:
-+            pass  # old conf, no second account tests can be run
-+        try:
-+            swift_test_user[2] = '%s%s' % ('%s:' % conf['account'] if 'account'
-+                                           in conf else '', conf['username3'])
-+            swift_test_key[2] = conf['password3']
-+        except KeyError as err:
-+            pass  # old conf, no third account tests can be run
-+
-+        for _ in range(3):
-+            swift_test_perm[_] = swift_test_user[_]
-+
-+    else:
-+        swift_test_user[0] = conf['username']
-+        swift_test_tenant[0] = conf['account']
-+        swift_test_key[0] = conf['password']
-+        swift_test_user[1] = conf['username2']
-+        swift_test_tenant[1] = conf['account2']
-+        swift_test_key[1] = conf['password2']
-+        swift_test_user[2] = conf['username3']
-+        swift_test_tenant[2] = conf['account']
-+        swift_test_key[2] = conf['password3']
-+
-+        for _ in range(3):
-+            swift_test_perm[_] = swift_test_tenant[_] + ':' \
-+                + swift_test_user[_]
-+
-+skip = not all([swift_test_auth, swift_test_user[0], swift_test_key[0]])
-+if skip:
-+    print >>sys.stderr, 'SKIPPING FUNCTIONAL TESTS DUE TO NO CONFIG'
-+
-+skip2 = not all([not skip, swift_test_user[1], swift_test_key[1]])
-+if not skip and skip2:
-+    print >>sys.stderr, \
-+        'SKIPPING SECOND ACCOUNT FUNCTIONAL TESTS DUE TO NO CONFIG FOR THEM'
-+
-+skip3 = not all([not skip, swift_test_user[2], swift_test_key[2]])
-+if not skip and skip3:
-+    print >>sys.stderr, \
-+        'SKIPPING THIRD ACCOUNT FUNCTIONAL TESTS DUE TO NO CONFIG FOR THEM'
-+
-+
-+class AuthError(Exception):
-+    pass
-+
-+
-+class InternalServerError(Exception):
-+    pass
-+
-+
-+url = [None, None, None]
-+token = [None, None, None]
-+parsed = [None, None, None]
-+conn = [None, None, None]
-+
-+
-+def retry(func, *args, **kwargs):
-+    """
-+    You can use the kwargs to override:
-+      'retries' (default: 5)
-+      'use_account' (default: 1) - which user's token to pass
-+      'url_account' (default: matches 'use_account') - which user's storage URL
-+      'resource' (default: url[url_account] - URL to connect to; retry()
-+          will interpolate the variable :storage_url: if present
-+    """
-+    global url, token, parsed, conn
-+    retries = kwargs.get('retries', 5)
-+    attempts, backoff = 0, 1
-+
-+    # use account #1 by default; turn user's 1-indexed account into 0-indexed
-+    use_account = kwargs.pop('use_account', 1) - 1
-+
-+    # access our own account by default
-+    url_account = kwargs.pop('url_account', use_account + 1) - 1
-+
-+    while attempts <= retries:
-+        attempts += 1
-+        try:
-+            if not url[use_account] or not token[use_account]:
-+                url[use_account], token[use_account] = \
-+                    get_auth(swift_test_auth, swift_test_user[use_account],
-+                             swift_test_key[use_account],
-+                             snet=False,
-+                             tenant_name=swift_test_tenant[use_account],
-+                             auth_version=swift_test_auth_version,
-+                             os_options={})
-+                parsed[use_account] = conn[use_account] = None
-+            if not parsed[use_account] or not conn[use_account]:
-+                parsed[use_account], conn[use_account] = \
-+                    http_connection(url[use_account])
-+
-+            # default resource is the account url[url_account]
-+            resource = kwargs.pop('resource', '%(storage_url)s')
-+            template_vars = {'storage_url': url[url_account]}
-+            parsed_result = urlparse(resource % template_vars)
-+            return func(url[url_account], token[use_account],
-+                        parsed_result, conn[url_account],
-+                        *args, **kwargs)
-+        except (socket.error, HTTPException):
-+            if attempts > retries:
-+                raise
-+            parsed[use_account] = conn[use_account] = None
-+        except AuthError:
-+            url[use_account] = token[use_account] = None
-+            continue
-+        except InternalServerError:
-+            pass
-+        if attempts <= retries:
-+            sleep(backoff)
-+            backoff *= 2
-+    raise Exception('No result after %s retries.' % retries)
-+
-+
-+def check_response(conn):
-+    resp = conn.getresponse()
-+    if resp.status == 401:
-+        resp.read()
-+        raise AuthError()
-+    elif resp.status // 100 == 5:
-+        resp.read()
-+        raise InternalServerError()
-+    return resp
-+
-+cluster_info = {}
-+
-+
-+def get_cluster_info():
-+    conn = Connection(conf)
-+    conn.authenticate()
-+    global cluster_info
-+    cluster_info = conn.cluster_info()
-+
-+
-+def reset_acl():
-+    def post(url, token, parsed, conn):
-+        conn.request('POST', parsed.path, '', {
-+            'X-Auth-Token': token,
-+            'X-Account-Access-Control': '{}'
-+        })
-+        return check_response(conn)
-+    resp = retry(post, use_account=1)
-+    resp.read()
-+
-+
-+def requires_acls(f):
-+    @functools.wraps(f)
-+    def wrapper(*args, **kwargs):
-+        if skip:
-+            raise SkipTest
-+        if not cluster_info:
-+            get_cluster_info()
-+        # Determine whether this cluster has account ACLs; if not, skip test
-+        if not cluster_info.get('tempauth', {}).get('account_acls'):
-+            raise SkipTest
-+        if 'keystoneauth' in cluster_info:
-+            # remove when keystoneauth supports account acls
-+            raise SkipTest
-+        reset_acl()
-+        try:
-+            rv = f(*args, **kwargs)
-+        finally:
-+            reset_acl()
-+        return rv
-+    return wrapper
---- a/test/functional/tests.py
-+++ b/test/functional/tests.py
-@@ -1624,5 +1624,466 @@ class TestFileComparison(Base):
- class TestFileComparisonUTF8(Base2, TestFileComparison):
-     set_up = False
- 
-+import json
-+from uuid import uuid4
-+from swift_testing import (check_response, retry, skip,
-+                           web_front_end, requires_acls)
-+import swift_testing
-+
-+class TestCVE20147960Container(unittest.TestCase):
-+
-+    def setUp(self):
-+        if skip:
-+            raise SkipTest
-+        self.name = uuid4().hex
-+        # this container isn't created by default, but will be cleaned up
-+        self.container = uuid4().hex
-+
-+        def put(url, token, parsed, conn):
-+            conn.request('PUT', parsed.path + '/' + self.name, '',
-+                         {'X-Auth-Token': token})
-+            return check_response(conn)
-+
-+        resp = retry(put)
-+        resp.read()
-+        self.assertEqual(resp.status, 201)
-+
-+        self.max_meta_count = load_constraint('max_meta_count')
-+        self.max_meta_name_length = load_constraint('max_meta_name_length')
-+        self.max_meta_overall_size = load_constraint('max_meta_overall_size')
-+        self.max_meta_value_length = load_constraint('max_meta_value_length')
-+
-+    def tearDown(self):
-+        if skip:
-+            raise SkipTest
-+
-+        def get(url, token, parsed, conn, container):
-+            conn.request(
-+                'GET', parsed.path + '/' + container + '?format=json', '',
-+                {'X-Auth-Token': token})
-+            return check_response(conn)
-+
-+        def delete(url, token, parsed, conn, container, obj):
-+            conn.request(
-+                'DELETE', '/'.join([parsed.path, container, obj['name']]), '',
-+                {'X-Auth-Token': token})
-+            return check_response(conn)
-+
-+        for container in (self.name, self.container):
-+            while True:
-+                resp = retry(get, container)
-+                body = resp.read()
-+                if resp.status == 404:
-+                    break
-+                self.assert_(resp.status // 100 == 2, resp.status)
-+                objs = json.loads(body)
-+                if not objs:
-+                    break
-+                for obj in objs:
-+                    resp = retry(delete, container, obj)
-+                    resp.read()
-+                    self.assertEqual(resp.status, 204)
-+
-+        def delete(url, token, parsed, conn, container):
-+            conn.request('DELETE', parsed.path + '/' + container, '',
-+                         {'X-Auth-Token': token})
-+            return check_response(conn)
-+
-+        resp = retry(delete, self.name)
-+        resp.read()
-+        self.assertEqual(resp.status, 204)
-+
-+        # container may have not been created
-+        resp = retry(delete, self.container)
-+        resp.read()
-+        self.assert_(resp.status in (204, 404))
-+
-+    def test_POST_bad_metadata(self):
-+        if skip:
-+            raise SkipTest
-+
-+        def post(url, token, parsed, conn, extra_headers):
-+            headers = {'X-Auth-Token': token}
-+            headers.update(extra_headers)
-+            conn.request('POST', parsed.path + '/' + self.name, '', headers)
-+            return check_response(conn)
-+
-+        resp = retry(
-+            post,
-+            {'X-Container-Meta-' + ('k' * self.max_meta_name_length): 'v'})
-+        resp.read()
-+        self.assertEqual(resp.status, 204)
-+        resp = retry(
-+            post,
-+            {'X-Container-Meta-' + (
-+                'k' * (self.max_meta_name_length + 1)): 'v'})
-+        resp.read()
-+        self.assertEqual(resp.status, 400)
-+
-+        resp = retry(
-+            post,
-+            {'X-Container-Meta-Too-Long': 'k' * self.max_meta_value_length})
-+        resp.read()
-+        self.assertEqual(resp.status, 204)
-+        resp = retry(
-+            post,
-+            {'X-Container-Meta-Too-Long': 'k' * (
-+                self.max_meta_value_length + 1)})
-+        resp.read()
-+        self.assertEqual(resp.status, 400)
-+
-+    def test_POST_maxcount_metadata(self):
-+        if skip:
-+            raise SkipTest
-+
-+        def post(url, token, parsed, conn, extra_headers):
-+            headers = {'X-Auth-Token': token}
-+            headers.update(extra_headers)
-+            conn.request('POST', parsed.path + '/' + self.name, '', headers)
-+            return check_response(conn)
-+
-+        headers = {}
-+        for x in xrange(self.max_meta_count):
-+            headers['X-Container-Meta-%d' % x] = 'v'
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 204)
-+
-+    def test_POST_maxcount_metadata_over_one(self):
-+        if skip:
-+            raise SkipTest
-+
-+        def post(url, token, parsed, conn, extra_headers):
-+            headers = {'X-Auth-Token': token}
-+            headers.update(extra_headers)
-+            conn.request('POST', parsed.path + '/' + self.name, '', headers)
-+            return check_response(conn)
-+
-+        headers = {}
-+        for x in xrange(self.max_meta_count + 1):
-+            headers['X-Container-Meta-%d' % x] = 'v'
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 400)
-+
-+    def test_POST_maxcount_metadata_over_two(self):
-+        if skip:
-+            raise SkipTest
-+
-+        def post(url, token, parsed, conn, extra_headers):
-+            headers = {'X-Auth-Token': token}
-+            headers.update(extra_headers)
-+            conn.request('POST', parsed.path + '/' + self.name, '', headers)
-+            return check_response(conn)
-+
-+        headers = {}
-+        for x in xrange(self.max_meta_count - 1):
-+            headers['X-Container-Meta-%d' % x] = 'v'
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 204)
-+
-+        headers = {}
-+        for x in xrange(self.max_meta_count - 1, self.max_meta_count + 1):
-+            headers['X-Container-Meta-%d' % x] = 'v'
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 400)
-+
-+    def test_POST_maxsize_metadata(self):
-+        if skip:
-+            raise SkipTest
-+
-+        def post(url, token, parsed, conn, extra_headers):
-+            headers = {'X-Auth-Token': token}
-+            headers.update(extra_headers)
-+            conn.request('POST', parsed.path + '/' + self.name, '', headers)
-+            return check_response(conn)
-+
-+        headers = {}
-+        header_value = 'k' * self.max_meta_value_length
-+        size = 0
-+        x = 0
-+        while size < (self.max_meta_overall_size - 4
-+                      - self.max_meta_value_length):
-+            size += 4 + self.max_meta_value_length
-+            headers['X-Container-Meta-%04d' % x] = header_value
-+            x += 1
-+        if self.max_meta_overall_size - size > 1:
-+            headers['X-Container-Meta-k'] = \
-+                'v' * (self.max_meta_overall_size - size - 1)
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 204)
-+
-+    def test_POST_maxsize_metadata_over_one(self):
-+        if skip:
-+            raise SkipTest
-+
-+        def post(url, token, parsed, conn, extra_headers):
-+            headers = {'X-Auth-Token': token}
-+            headers.update(extra_headers)
-+            conn.request('POST', parsed.path + '/' + self.name, '', headers)
-+            return check_response(conn)
-+
-+        headers = {}
-+        header_value = 'k' * self.max_meta_value_length
-+        size = 0
-+        x = 0
-+        while size < (self.max_meta_overall_size - 4
-+                      - self.max_meta_value_length):
-+            size += 4 + self.max_meta_value_length
-+            headers['X-Container-Meta-%04d' % x] = header_value
-+            x += 1
-+        if self.max_meta_overall_size >= size:
-+            headers['X-Container-Meta-k'] = \
-+                'v' * (self.max_meta_overall_size - size)
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 400)
-+
-+    def test_POST_maxsize_metadata_over_two(self):
-+        if skip:
-+            raise SkipTest
-+
-+        def post(url, token, parsed, conn, extra_headers):
-+            headers = {'X-Auth-Token': token}
-+            headers.update(extra_headers)
-+            conn.request('POST', parsed.path + '/' + self.name, '', headers)
-+            return check_response(conn)
-+
-+        headers = {}
-+        header_value = 'k' * self.max_meta_value_length
-+        size = 0
-+        x = 0
-+        while size < (self.max_meta_overall_size - 4
-+                      - self.max_meta_value_length):
-+            size += 4 + self.max_meta_value_length
-+            headers['X-Container-Meta-%04d' % x] = header_value
-+            x += 1
-+        if self.max_meta_overall_size - size > 1:
-+            headers['X-Container-Meta-k'] = \
-+                'v' * (self.max_meta_overall_size - size - 1)
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 204)
-+        headers = {}
-+        headers['X-Container-Meta-k2'] = 'v'
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 400)
-+
-+
-+class TestCVE20147960Account(unittest.TestCase):
-+
-+    def setUp(self):
-+        self.max_meta_count = load_constraint('max_meta_count')
-+        self.max_meta_name_length = load_constraint('max_meta_name_length')
-+        self.max_meta_overall_size = load_constraint('max_meta_overall_size')
-+        self.max_meta_value_length = load_constraint('max_meta_value_length')
-+
-+        def head(url, token, parsed, conn):
-+            conn.request('HEAD', parsed.path, '', {'X-Auth-Token': token})
-+            return check_response(conn)
-+        resp = retry(head)
-+        self.existing_metadata = set([
-+            k for k, v in resp.getheaders() if
-+            k.lower().startswith('x-account-meta')])
-+
-+    def tearDown(self):
-+        def head(url, token, parsed, conn):
-+            conn.request('HEAD', parsed.path, '', {'X-Auth-Token': token})
-+            return check_response(conn)
-+        resp = retry(head)
-+        resp.read()
-+        new_metadata = set(
-+            [k for k, v in resp.getheaders() if
-+             k.lower().startswith('x-account-meta')])
-+
-+        def clear_meta(url, token, parsed, conn, remove_metadata_keys):
-+            headers = {'X-Auth-Token': token}
-+            headers.update((k, '') for k in remove_metadata_keys)
-+            conn.request('POST', parsed.path, '', headers)
-+            return check_response(conn)
-+        extra_metadata = list(self.existing_metadata ^ new_metadata)
-+        for i in range(0, len(extra_metadata), 90):
-+            batch = extra_metadata[i:i + 90]
-+            resp = retry(clear_meta, batch)
-+            resp.read()
-+            self.assertEqual(resp.status // 100, 2)
-+
-+    def test_maxcount_metadata(self):
-+        if skip:
-+            raise SkipTest
-+
-+        def post(url, token, parsed, conn, extra_headers):
-+            headers = {'X-Auth-Token': token}
-+            headers.update(extra_headers)
-+            conn.request('POST', parsed.path, '', headers)
-+            return check_response(conn)
-+
-+        # TODO: Find the test that adds these and remove them.
-+        headers = {'x-remove-account-meta-temp-url-key': 'remove',
-+                   'x-remove-account-meta-temp-url-key-2': 'remove'}
-+        resp = retry(post, headers)
-+
-+        headers = {}
-+        for x in xrange(MAX_META_COUNT):
-+            headers['X-Account-Meta-%d' % x] = 'v'
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 204)
-+
-+    def test_maxcount_metadata_over_one(self):
-+        if skip:
-+            raise SkipTest
-+
-+        def post(url, token, parsed, conn, extra_headers):
-+            headers = {'X-Auth-Token': token}
-+            headers.update(extra_headers)
-+            conn.request('POST', parsed.path, '', headers)
-+            return check_response(conn)
-+
-+        # TODO: Find the test that adds these and remove them.
-+        headers = {'x-remove-account-meta-temp-url-key': 'remove',
-+                   'x-remove-account-meta-temp-url-key-2': 'remove'}
-+        resp = retry(post, headers)
-+
-+        headers = {}
-+        for x in xrange(MAX_META_COUNT + 1):
-+            headers['X-Account-Meta-%d' % x] = 'v'
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 400)
-+
-+    def test_maxcount_metadata_over_two(self):
-+        if skip:
-+            raise SkipTest
-+
-+        def post(url, token, parsed, conn, extra_headers):
-+            headers = {'X-Auth-Token': token}
-+            headers.update(extra_headers)
-+            conn.request('POST', parsed.path, '', headers)
-+            return check_response(conn)
-+
-+        # TODO: Find the test that adds these and remove them.
-+        headers = {'x-remove-account-meta-temp-url-key': 'remove',
-+                   'x-remove-account-meta-temp-url-key-2': 'remove'}
-+        resp = retry(post, headers)
-+
-+        headers = {}
-+        for x in xrange(MAX_META_COUNT - 1):
-+            headers['X-Account-Meta-%d' % x] = 'v'
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 204)
-+
-+        headers = {}
-+        for x in xrange(MAX_META_COUNT - 1, MAX_META_COUNT + 1):
-+            headers['X-Account-Meta-%d' % x] = 'v'
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 400)
-+
-+    def test_maxsize_metadata(self):
-+        if skip:
-+            raise SkipTest
-+
-+        def post(url, token, parsed, conn, extra_headers):
-+            headers = {'X-Auth-Token': token}
-+            headers.update(extra_headers)
-+            conn.request('POST', parsed.path, '', headers)
-+            return check_response(conn)
-+
-+        # TODO: Find the test that adds these and remove them.
-+        headers = {'x-remove-account-meta-temp-url-key': 'remove',
-+                   'x-remove-account-meta-temp-url-key-2': 'remove'}
-+        resp = retry(post, headers)
-+
-+        headers = {}
-+        header_value = 'k' * MAX_META_VALUE_LENGTH
-+        size = 0
-+        x = 0
-+        while size < MAX_META_OVERALL_SIZE - 4 - MAX_META_VALUE_LENGTH:
-+            size += 4 + MAX_META_VALUE_LENGTH
-+            headers['X-Account-Meta-%04d' % x] = header_value
-+            x += 1
-+        if MAX_META_OVERALL_SIZE - size > 1:
-+            headers['X-Account-Meta-k'] = \
-+                'v' * (MAX_META_OVERALL_SIZE - size - 1)
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 204)
-+        headers['X-Account-Meta-k'] = \
-+            'v' * (MAX_META_OVERALL_SIZE - size)
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 400)
-+
-+    def test_maxsize_metadata_over_one(self):
-+        if skip:
-+            raise SkipTest
-+
-+        def post(url, token, parsed, conn, extra_headers):
-+            headers = {'X-Auth-Token': token}
-+            headers.update(extra_headers)
-+            conn.request('POST', parsed.path, '', headers)
-+            return check_response(conn)
-+
-+        # TODO: Find the test that adds these and remove them.
-+        headers = {'x-remove-account-meta-temp-url-key': 'remove',
-+                   'x-remove-account-meta-temp-url-key-2': 'remove'}
-+        resp = retry(post, headers)
-+
-+        headers = {}
-+        header_value = 'k' * MAX_META_VALUE_LENGTH
-+        size = 0
-+        x = 0
-+        while size < MAX_META_OVERALL_SIZE - 4 - MAX_META_VALUE_LENGTH:
-+            size += 4 + MAX_META_VALUE_LENGTH
-+            headers['X-Account-Meta-%04d' % x] = header_value
-+            x += 1
-+        if MAX_META_OVERALL_SIZE >= size:
-+            headers['X-Account-Meta-k'] = \
-+                'v' * (MAX_META_OVERALL_SIZE - size)
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 400)
-+
-+    def test_maxsize_metadata_over_two(self):
-+        if skip:
-+            raise SkipTest
-+
-+        def post(url, token, parsed, conn, extra_headers):
-+            headers = {'X-Auth-Token': token}
-+            headers.update(extra_headers)
-+            conn.request('POST', parsed.path, '', headers)
-+            return check_response(conn)
-+
-+        # TODO: Find the test that adds these and remove them.
-+        headers = {'x-remove-account-meta-temp-url-key': 'remove',
-+                   'x-remove-account-meta-temp-url-key-2': 'remove'}
-+        resp = retry(post, headers)
-+
-+        headers = {}
-+        header_value = 'k' * MAX_META_VALUE_LENGTH
-+        size = 0
-+        x = 0
-+        while size < MAX_META_OVERALL_SIZE - 4 - MAX_META_VALUE_LENGTH:
-+            size += 4 + MAX_META_VALUE_LENGTH
-+            headers['X-Account-Meta-%04d' % x] = header_value
-+            x += 1
-+        if MAX_META_OVERALL_SIZE - size > 1:
-+            headers['X-Account-Meta-k'] = \
-+                'v' * (MAX_META_OVERALL_SIZE - size - 1)
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 204)
-+        headers = {}
-+        headers['X-Account-Meta-k2'] = 'v'
-+        resp = retry(post, headers)
-+        resp.read()
-+        self.assertEqual(resp.status, 400)
-+
- if __name__ == '__main__':
-     unittest.main()
---- a/test/unit/common/test_db.py
-+++ b/test/unit/common/test_db.py
-@@ -26,10 +26,13 @@ import sqlite3
- from mock import patch
- 
- import swift.common.db
-+from swift.common.constraints import \
-+    MAX_META_VALUE_LENGTH, MAX_META_COUNT, MAX_META_OVERALL_SIZE
- from swift.common.db import chexor, dict_factory, get_db_connection, \
-     DatabaseBroker, DatabaseConnectionError, DatabaseAlreadyExists
- from swift.common.utils import normalize_timestamp
- from swift.common.exceptions import LockTimeout
-+from swift.common.swob import HTTPException
- 
- 
- class TestDatabaseConnectionError(unittest.TestCase):
-@@ -181,7 +184,7 @@ class TestDatabaseBroker(unittest.TestCa
-             conn.execute('CREATE TABLE test (one TEXT)')
-             conn.execute('CREATE TABLE test_stat (id TEXT)')
-             conn.execute('INSERT INTO test_stat (id) VALUES (?)',
--                        (str(uuid4),))
-+                         (str(uuid4),))
-             conn.execute('INSERT INTO test (one) VALUES ("1")')
-             conn.commit()
-         stub_called = [False]
-@@ -632,6 +635,91 @@ class TestDatabaseBroker(unittest.TestCa
-                           [first_value, first_timestamp])
-         self.assert_('Second' not in broker.metadata)
- 
-+    @patch.object(DatabaseBroker, 'validate_metadata')
-+    def test_validate_metadata_is_called_from_update_metadata(self, mock):
-+        broker = self.get_replication_info_tester(metadata=True)
-+        first_timestamp = normalize_timestamp(1)
-+        first_value = '1'
-+        metadata = {'First': [first_value, first_timestamp]}
-+        broker.update_metadata(metadata, validate_metadata=True)
-+        self.assertTrue(mock.called)
-+
-+    @patch.object(DatabaseBroker, 'validate_metadata')
-+    def test_validate_metadata_is_not_called_from_update_metadata(self, mock):
-+        broker = self.get_replication_info_tester(metadata=True)
-+        first_timestamp = normalize_timestamp(1)
-+        first_value = '1'
-+        metadata = {'First': [first_value, first_timestamp]}
-+        broker.update_metadata(metadata)
-+        self.assertFalse(mock.called)
-+
-+    def test_metadata_with_max_count(self):
-+        metadata = {}
-+        for c in xrange(MAX_META_COUNT):
-+            key = 'X-Account-Meta-F{0}'.format(c)
-+            metadata[key] = ('B', normalize_timestamp(1))
-+        key = 'X-Account-Meta-Foo'.format(c)
-+        metadata[key] = ('', normalize_timestamp(1))
-+        try:
-+            DatabaseBroker.validate_metadata(metadata)
-+        except HTTPException:
-+            self.fail('Unexpected HTTPException')
-+
-+    def test_metadata_raises_exception_over_max_count(self):
-+        metadata = {}
-+        for c in xrange(MAX_META_COUNT + 1):
-+            key = 'X-Account-Meta-F{0}'.format(c)
-+            metadata[key] = ('B', normalize_timestamp(1))
-+        message = ''
-+        try:
-+            DatabaseBroker.validate_metadata(metadata)
-+        except HTTPException as e:
-+            message = str(e)
-+        self.assertEqual(message, '400 Bad Request')
-+
-+    def test_metadata_with_max_overall_size(self):
-+        metadata = {}
-+        metadata_value = 'v' * MAX_META_VALUE_LENGTH
-+        size = 0
-+        x = 0
-+        while size < (MAX_META_OVERALL_SIZE - 4
-+                      - MAX_META_VALUE_LENGTH):
-+            size += 4 + MAX_META_VALUE_LENGTH
-+            metadata['X-Account-Meta-%04d' % x] = (metadata_value,
-+                                                   normalize_timestamp(1))
-+            x += 1
-+        if MAX_META_OVERALL_SIZE - size > 1:
-+            metadata['X-Account-Meta-k'] = (
-+                'v' * (MAX_META_OVERALL_SIZE - size - 1),
-+                normalize_timestamp(1))
-+        try:
-+            DatabaseBroker.validate_metadata(metadata)
-+        except HTTPException:
-+            self.fail('Unexpected HTTPException')
-+
-+    def test_metadata_raises_exception_over_max_overall_size(self):
-+        metadata = {}
-+        metadata_value = 'k' * MAX_META_VALUE_LENGTH
-+        size = 0
-+        x = 0
-+        while size < (MAX_META_OVERALL_SIZE - 4
-+                      - MAX_META_VALUE_LENGTH):
-+            size += 4 + MAX_META_VALUE_LENGTH
-+            metadata['X-Account-Meta-%04d' % x] = (metadata_value,
-+                                                   normalize_timestamp(1))
-+            x += 1
-+        if MAX_META_OVERALL_SIZE - size > 1:
-+            metadata['X-Account-Meta-k'] = (
-+                'v' * (MAX_META_OVERALL_SIZE - size - 1),
-+                normalize_timestamp(1))
-+        metadata['X-Account-Meta-k2'] = ('v', normalize_timestamp(1))
-+        message = ''
-+        try:
-+            DatabaseBroker.validate_metadata(metadata)
-+        except HTTPException as e:
-+            message = str(e)
-+        self.assertEqual(message, '400 Bad Request')
-+
- 
- if __name__ == '__main__':
-     unittest.main()