From 8333c4b36aee2102a6f2721e96994b5b9e67d8ee Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Fri, 29 Jan 2016 13:29:31 +0100 Subject: [PATCH] tempurls with a prefix-based scope The middleware now allows the usage of signatures with a prefix-based scope. A prefix-based signature grants access to all objects which share the same prefix. This avoids the creation of a large amount of signatures, when a whole container or pseudofolder is shared. Please see spec: https://review.openstack.org/#/c/199607/ Change-Id: I03b68eb74dae6196b5e63e711ef642ff7d2cfdc9 --- doc/source/api/temporary_url_middleware.rst | 39 ++++- swift/common/middleware/tempurl.py | 99 ++++++++---- test/functional/test_tempurl.py | 115 ++++++++++--- test/unit/common/middleware/test_tempurl.py | 170 +++++++++++++++----- 4 files changed, 321 insertions(+), 102 deletions(-) diff --git a/doc/source/api/temporary_url_middleware.rst b/doc/source/api/temporary_url_middleware.rst index 1debf7d542..968c58e87a 100644 --- a/doc/source/api/temporary_url_middleware.rst +++ b/doc/source/api/temporary_url_middleware.rst @@ -14,6 +14,10 @@ the object. When the web browser user clicks on the link, the browser downloads the object directly from Object Storage, eliminating the need for the website to act as a proxy for the request. +Furthermore, a temporary URL can be prefix-based. These URLs +contain a signature which is is valid for all objects which share +a common prefix. They are useful for sharing a set of objects. + Ask your cloud administrator to enable the temporary URL feature. For information, see :ref:`tempurl` in the *Source Documentation*. @@ -60,6 +64,17 @@ object name. Object Storage returns this value in the ``Content-Disposition`` response header. Browsers can interpret this file name value as a file attachment to be saved. +A prefix-based temporary URL is similar but requires the parameter +``temp_url_prefix``, which must be equal to the common prefix shared +by all object names for which the URL is valid. + +.. code:: + + https://swift-cluster.example.com/v1/my_account/container/my_prefix/object + ?temp_url_sig=da39a3ee5e6b4b0d3255bfef95601890afd80709 + &temp_url_expires=1323479485 + &temp_url_prefix=my_prefix + .. _secret_keys: Secret Keys @@ -114,17 +129,16 @@ signature includes these elements: into the future. - The path. Starting with ``/v1/`` onwards and including a container - name and object. In the example below, the path is - ``/v1/my_account/container/object``. Do not URL-encode the path at - this stage. + name and object. The path for prefix-based signatures must start with + ``prefix:/v1/``. Do not URL-encode the path at this stage. - The secret key. Use one of the key values as described in :ref:`secret_keys`. -This sample Python code shows how to compute a signature for use with +These sample Python codes show how to compute a signature for use with temporary URLs: -**Example HMAC-SHA1 signature for temporary URLs** +**Example HMAC-SHA1 signature for object-based temporary URLs** .. code:: @@ -139,6 +153,21 @@ temporary URLs: hmac_body = '%s\n%s\n%s' % (method, expires, path) signature = hmac.new(key, hmac_body, sha1).hexdigest() +**Example HMAC-SHA1 signature for prefix-based temporary URLs** + +.. code:: + + import hmac + from hashlib import sha1 + from time import time + method = 'GET' + duration_in_seconds = 60*60*24 + expires = int(time() + duration_in_seconds) + path = 'prefix:/v1/my_account/container/my_prefix' + key = 'MYKEY' + hmac_body = '%s\n%s\n%s' % (method, expires, path) + signature = hmac.new(key, hmac_body, sha1).hexdigest() + Do not URL-encode the path when you generate the HMAC-SHA1 signature. However, when you make the actual HTTP request, you should properly diff --git a/swift/common/middleware/tempurl.py b/swift/common/middleware/tempurl.py index 8add9762bf..cf140edcba 100644 --- a/swift/common/middleware/tempurl.py +++ b/swift/common/middleware/tempurl.py @@ -44,11 +44,16 @@ If the user were to share the link with all his friends, or accidentally post it on a forum, etc. the direct access would be limited to the expiration time set when the website created the link. +Beyond that, the middleware provides the ability to create URLs, which +contain signatures which are valid for all objects which share a +common prefix. These prefix-based URLs are useful for sharing a set +of objects. + ------------ Client Usage ------------ -To create such temporary URLs, first an ``X-Account-Meta-Temp-URL-Key`` +To create temporary URLs, first an ``X-Account-Meta-Temp-URL-Key`` header must be set on the Swift account. Then, an HMAC-SHA1 (RFC 2104) signature is generated using the HTTP method to allow (``GET``, ``PUT``, ``DELETE``, etc.), the Unix timestamp the access should be allowed until, @@ -77,10 +82,32 @@ Let's say ``sig`` ends up equaling temp_url_sig=da39a3ee5e6b4b0d3255bfef95601890afd80709& temp_url_expires=1323479485 -Any alteration of the resource path or query arguments would result in -``401 Unauthorized``. Similarly, a ``PUT`` where ``GET`` was the allowed method -would be rejected with ``401 Unauthorized``. However, ``HEAD`` is allowed if -``GET``, ``PUT``, or ``POST`` is allowed. +If a prefix-based signature with the prefix ``pre`` is desired, set path to:: + + path = 'prefix:/v1/AUTH_account/container/pre' + +The generated signature would be valid for all objects starting +with ``pre``. The middleware detects a prefix-based temporary URL by +a query parameter called ``temp_url_prefix``. So, if ``sig`` and ``expires`` +would end up like above, following URL would be valid:: + + https://swift-cluster.example.com/v1/AUTH_account/container/pre/object? + temp_url_sig=da39a3ee5e6b4b0d3255bfef95601890afd80709& + temp_url_expires=1323479485& + temp_url_prefix=pre + +Another valid URL:: + + https://swift-cluster.example.com/v1/AUTH_account/container/pre/ + subfolder/another_object? + temp_url_sig=da39a3ee5e6b4b0d3255bfef95601890afd80709& + temp_url_expires=1323479485& + temp_url_prefix=pre + +Any alteration of the resource path or query arguments of a temporary URL +would result in ``401 Unauthorized``. Similarly, a ``PUT`` where ``GET`` was +the allowed method would be rejected with ``401 Unauthorized``. +However, ``HEAD`` is allowed if ``GET``, ``PUT``, or ``POST`` is allowed. Using this in combination with browser form post translation middleware could also allow direct-from-browser uploads to specific @@ -357,28 +384,36 @@ class TempURL(object): if env['REQUEST_METHOD'] == 'OPTIONS': return self.app(env, start_response) info = self._get_temp_url_info(env) - temp_url_sig, temp_url_expires, filename, inline_disposition = info + temp_url_sig, temp_url_expires, temp_url_prefix, filename,\ + inline_disposition = info if temp_url_sig is None and temp_url_expires is None: return self.app(env, start_response) if not temp_url_sig or not temp_url_expires: return self._invalid(env, start_response) - account, container = self._get_account_and_container(env) + account, container, obj = self._get_path_parts(env) if not account: return self._invalid(env, start_response) keys = self._get_keys(env) if not keys: return self._invalid(env, start_response) + if temp_url_prefix is None: + path = '/v1/%s/%s/%s' % (account, container, obj) + else: + if not obj.startswith(temp_url_prefix): + return self._invalid(env, start_response) + path = 'prefix:/v1/%s/%s/%s' % (account, container, + temp_url_prefix) if env['REQUEST_METHOD'] == 'HEAD': hmac_vals = ( - self._get_hmacs(env, temp_url_expires, keys) + - self._get_hmacs(env, temp_url_expires, keys, + self._get_hmacs(env, temp_url_expires, path, keys) + + self._get_hmacs(env, temp_url_expires, path, keys, request_method='GET') + - self._get_hmacs(env, temp_url_expires, keys, + self._get_hmacs(env, temp_url_expires, path, keys, request_method='POST') + - self._get_hmacs(env, temp_url_expires, keys, + self._get_hmacs(env, temp_url_expires, path, keys, request_method='PUT')) else: - hmac_vals = self._get_hmacs(env, temp_url_expires, keys) + hmac_vals = self._get_hmacs(env, temp_url_expires, path, keys) is_valid_hmac = False hmac_scope = None @@ -410,6 +445,8 @@ class TempURL(object): env['REMOTE_USER'] = '.wsgi.tempurl' qs = {'temp_url_sig': temp_url_sig, 'temp_url_expires': temp_url_expires} + if temp_url_prefix is not None: + qs['temp_url_prefix'] = temp_url_prefix if filename: qs['filename'] = filename env['QUERY_STRING'] = urlencode(qs) @@ -457,36 +494,38 @@ class TempURL(object): return self.app(env, _start_response) - def _get_account_and_container(self, env): + def _get_path_parts(self, env): """ - Returns just the account and container for the request, if it's an - object request and one of the configured methods; otherwise, None is - returned. + Return the account, container and object name for the request, + if it's an object request and one of the configured methods; + otherwise, None is returned. :param env: The WSGI environment for the request. - :returns: (Account str, container str) or (None, None). + :returns: (Account str, container str, object str) or + (None, None, None). """ if env['REQUEST_METHOD'] in self.conf['methods']: try: ver, acc, cont, obj = split_path(env['PATH_INFO'], 4, 4, True) except ValueError: - return (None, None) + return (None, None, None) if ver == 'v1' and obj.strip('/'): - return (acc, cont) - return (None, None) + return (acc, cont, obj) + return (None, None, None) def _get_temp_url_info(self, env): """ - Returns the provided temporary URL parameters (sig, expires), - if given and syntactically valid. Either sig or expires could + Returns the provided temporary URL parameters (sig, expires, prefix), + if given and syntactically valid. Either sig, expires or prefix could be None if not provided. If provided, expires is also converted to an int if possible or 0 if not, and checked for expiration (returns 0 if expired). :param env: The WSGI environment for the request. - :returns: (sig, expires, filename, inline) as described above. + :returns: (sig, expires, prefix, filename, inline) as described above. """ - temp_url_sig = temp_url_expires = filename = inline = None + temp_url_sig = temp_url_expires = temp_url_prefix = filename =\ + inline = None qs = parse_qs(env.get('QUERY_STRING', ''), keep_blank_values=True) if 'temp_url_sig' in qs: temp_url_sig = qs['temp_url_sig'][0] @@ -497,11 +536,14 @@ class TempURL(object): temp_url_expires = 0 if temp_url_expires < time(): temp_url_expires = 0 + if 'temp_url_prefix' in qs: + temp_url_prefix = qs['temp_url_prefix'][0] if 'filename' in qs: filename = qs['filename'][0] if 'inline' in qs: inline = True - return temp_url_sig, temp_url_expires, filename, inline + return (temp_url_sig, temp_url_expires, temp_url_prefix, filename, + inline) def _get_keys(self, env): """ @@ -531,11 +573,13 @@ class TempURL(object): return ([(ak, ACCOUNT_SCOPE) for ak in account_keys] + [(ck, CONTAINER_SCOPE) for ck in container_keys]) - def _get_hmacs(self, env, expires, scoped_keys, request_method=None): + def _get_hmacs(self, env, expires, path, scoped_keys, + request_method=None): """ :param env: The WSGI environment for the request. :param expires: Unix timestamp as an int for when the URL expires. + :param path: The path which is used for hashing. :param scoped_keys: (key, scope) tuples like _get_keys() returns :param request_method: Optional override of the request in the WSGI env. For example, if a HEAD @@ -547,8 +591,9 @@ class TempURL(object): """ if not request_method: request_method = env['REQUEST_METHOD'] + return [ - (get_hmac(request_method, env['PATH_INFO'], expires, key), scope) + (get_hmac(request_method, path, expires, key), scope) for (key, scope) in scoped_keys] def _invalid(self, env, start_response): diff --git a/test/functional/test_tempurl.py b/test/functional/test_tempurl.py index a4e298d0e8..08b74b6c4c 100644 --- a/test/functional/test_tempurl.py +++ b/test/functional/test_tempurl.py @@ -87,17 +87,16 @@ class TestTempurl(Base): (self.env.tempurl_enabled,)) expires = int(time.time()) + 86400 - sig = self.tempurl_sig( + self.obj_tempurl_parms = self.tempurl_parms( 'GET', expires, self.env.conn.make_path(self.env.obj.path), self.env.tempurl_key) - self.obj_tempurl_parms = {'temp_url_sig': sig, - 'temp_url_expires': str(expires)} - def tempurl_sig(self, method, expires, path, key): - return hmac.new( + def tempurl_parms(self, method, expires, path, key): + sig = hmac.new( key, '%s\n%s\n%s' % (method, expires, urllib.parse.unquote(path)), hashlib.sha1).hexdigest() + return {'temp_url_sig': sig, 'temp_url_expires': str(expires)} def test_GET(self): contents = self.env.obj.read( @@ -111,11 +110,9 @@ class TestTempurl(Base): def test_GET_with_key_2(self): expires = int(time.time()) + 86400 - sig = self.tempurl_sig( + parms = self.tempurl_parms( 'GET', expires, self.env.conn.make_path(self.env.obj.path), self.env.tempurl_key2) - parms = {'temp_url_sig': sig, - 'temp_url_expires': str(expires)} contents = self.env.obj.read(parms=parms, cfg={'no_auth_token': True}) self.assertEqual(contents, "obj contents") @@ -135,11 +132,9 @@ class TestTempurl(Base): (self.env.container.name,)}) expires = int(time.time()) + 86400 - sig = self.tempurl_sig( + parms = self.tempurl_parms( 'GET', expires, self.env.conn.make_path(manifest.path), self.env.tempurl_key) - parms = {'temp_url_sig': sig, - 'temp_url_expires': str(expires)} contents = manifest.read(parms=parms, cfg={'no_auth_token': True}) self.assertEqual(contents, "one fish two fish red fish blue fish") @@ -162,11 +157,9 @@ class TestTempurl(Base): (self.env.container.name,)}) expires = int(time.time()) + 86400 - sig = self.tempurl_sig( + parms = self.tempurl_parms( 'GET', expires, self.env.conn.make_path(manifest.path), self.env.tempurl_key) - parms = {'temp_url_sig': sig, - 'temp_url_expires': str(expires)} # cross container tempurl works fine for account tempurl key contents = manifest.read(parms=parms, cfg={'no_auth_token': True}) @@ -177,11 +170,9 @@ class TestTempurl(Base): new_obj = self.env.container.file(Utils.create_name()) expires = int(time.time()) + 86400 - sig = self.tempurl_sig( + put_parms = self.tempurl_parms( 'PUT', expires, self.env.conn.make_path(new_obj.path), self.env.tempurl_key) - put_parms = {'temp_url_sig': sig, - 'temp_url_expires': str(expires)} new_obj.write('new obj contents', parms=put_parms, cfg={'no_auth_token': True}) @@ -196,11 +187,9 @@ class TestTempurl(Base): # give out a signature which allows a PUT to new_obj expires = int(time.time()) + 86400 - sig = self.tempurl_sig( + put_parms = self.tempurl_parms( 'PUT', expires, self.env.conn.make_path(new_obj.path), self.env.tempurl_key) - put_parms = {'temp_url_sig': sig, - 'temp_url_expires': str(expires)} # try to create manifest pointing to some random container try: @@ -230,11 +219,9 @@ class TestTempurl(Base): # try again using a tempurl POST to an already created object new_obj.write('', {}, parms=put_parms, cfg={'no_auth_token': True}) expires = int(time.time()) + 86400 - sig = self.tempurl_sig( + post_parms = self.tempurl_parms( 'POST', expires, self.env.conn.make_path(new_obj.path), self.env.tempurl_key) - post_parms = {'temp_url_sig': sig, - 'temp_url_expires': str(expires)} try: new_obj.post({'x-object-manifest': '%s/foo' % other_container}, parms=post_parms, cfg={'no_auth_token': True}) @@ -245,11 +232,9 @@ class TestTempurl(Base): def test_HEAD(self): expires = int(time.time()) + 86400 - sig = self.tempurl_sig( + head_parms = self.tempurl_parms( 'HEAD', expires, self.env.conn.make_path(self.env.obj.path), self.env.tempurl_key) - head_parms = {'temp_url_sig': sig, - 'temp_url_expires': str(expires)} self.assertTrue(self.env.obj.info(parms=head_parms, cfg={'no_auth_token': True})) @@ -312,6 +297,84 @@ class TestTempurl(Base): self.assert_status([401]) +class TestTempURLPrefix(TestTempurl): + def setUp(self): + super(TestTempurl, self).setUp() + if self.env.tempurl_enabled is False: + raise SkipTest("TempURL not enabled") + elif self.env.tempurl_enabled is not True: + # just some sanity checking + raise Exception( + "Expected tempurl_enabled to be True/False, got %r" % + (self.env.tempurl_enabled,)) + + self.expires = int(time.time()) + 86400 + self.obj_tempurl_parms = self.tempurl_parms( + 'GET', self.expires, + self.env.conn.make_path(self.env.obj.path), + self.env.tempurl_key) + + def tempurl_parms(self, method, expires, path, key, + prefix=None): + path_parts = urllib.parse.unquote(path).split('/') + + if prefix is None: + # Choose the first 4 chars of object name as prefix. + prefix = path_parts[4][0:4] + sig = hmac.new( + key, + '%s\n%s\nprefix:%s' % (method, expires, + '/'.join(path_parts[0:4]) + '/' + prefix), + hashlib.sha1).hexdigest() + return { + 'temp_url_sig': sig, 'temp_url_expires': str(expires), + 'temp_url_prefix': prefix} + + def test_empty_prefix(self): + parms = self.tempurl_parms( + 'GET', self.expires, + self.env.conn.make_path(self.env.obj.path), + self.env.tempurl_key, '') + + contents = self.env.obj.read( + parms=parms, + cfg={'no_auth_token': True}) + self.assertEqual(contents, "obj contents") + + def test_no_prefix_match(self): + prefix = 'b' if self.env.obj.name[0] == 'a' else 'a' + + parms = self.tempurl_parms( + 'GET', self.expires, + self.env.conn.make_path(self.env.obj.path), + self.env.tempurl_key, prefix) + + self.assertRaises(ResponseError, self.env.obj.read, + cfg={'no_auth_token': True}, + parms=parms) + self.assert_status([401]) + + def test_object_url_with_prefix(self): + parms = super(TestTempURLPrefix, self).tempurl_parms( + 'GET', self.expires, + self.env.conn.make_path(self.env.obj.path), + self.env.tempurl_key) + parms['temp_url_prefix'] = self.env.obj.name + + self.assertRaises(ResponseError, self.env.obj.read, + cfg={'no_auth_token': True}, + parms=parms) + self.assert_status([401]) + + def test_missing_query_parm(self): + del self.obj_tempurl_parms['temp_url_prefix'] + + self.assertRaises(ResponseError, self.env.obj.read, + cfg={'no_auth_token': True}, + parms=self.obj_tempurl_parms) + self.assert_status([401]) + + class TestTempurlUTF8(Base2, TestTempurl): set_up = False diff --git a/test/unit/common/middleware/test_tempurl.py b/test/unit/common/middleware/test_tempurl.py index 1b86c04e37..5db4e9532e 100644 --- a/test/unit/common/middleware/test_tempurl.py +++ b/test/unit/common/middleware/test_tempurl.py @@ -80,7 +80,7 @@ class TestTempURL(unittest.TestCase): if environ is None: environ = {} - _junk, account, _junk, _junk = utils.split_path(path, 2, 4) + _junk, account, _junk, _junk = utils.split_path(path, 2, 4, True) self._fake_cache_environ(environ, account, keys, container_keys=container_keys) req = Request.blank(path, environ=environ, **kwargs) @@ -125,11 +125,14 @@ class TestTempURL(unittest.TestCase): environ={'REQUEST_METHOD': 'OPTIONS'}).get_response(self.tempurl) self.assertEqual(resp.status_int, 200) - def assert_valid_sig(self, expires, path, keys, sig, environ=None): + def assert_valid_sig(self, expires, path, keys, sig, environ=None, + prefix=None): if not environ: environ = {} environ['QUERY_STRING'] = 'temp_url_sig=%s&temp_url_expires=%s' % ( sig, expires) + if prefix is not None: + environ['QUERY_STRING'] += '&temp_url_prefix=%s' % prefix req = self._make_request(path, keys=keys, environ=environ) self.tempurl.app = FakeApp(iter([('200 Ok', (), '123')])) resp = req.get_response(self.tempurl) @@ -294,6 +297,33 @@ class TestTempURL(unittest.TestCase): self.assertEqual(req.environ['swift.authorize_override'], True) self.assertEqual(req.environ['REMOTE_USER'], '.wsgi.tempurl') + def test_get_valid_with_prefix(self): + method = 'GET' + expires = int(time() + 86400) + prefix = 'p1/p2/' + sig_path = 'prefix:/v1/a/c/' + prefix + query_path = '/v1/a/c/' + prefix + 'o' + key = 'abc' + hmac_body = '%s\n%s\n%s' % (method, expires, sig_path) + sig = hmac.new(key, hmac_body, sha1).hexdigest() + self.assert_valid_sig(expires, query_path, [key], sig, prefix=prefix) + + query_path = query_path[:-1] + 'p3/o' + key = 'abc' + hmac_body = '%s\n%s\n%s' % (method, expires, sig_path) + sig = hmac.new(key, hmac_body, sha1).hexdigest() + self.assert_valid_sig(expires, query_path, [key], sig, prefix=prefix) + + def test_get_valid_with_prefix_empty(self): + method = 'GET' + expires = int(time() + 86400) + sig_path = 'prefix:/v1/a/c/' + query_path = '/v1/a/c/o' + key = 'abc' + hmac_body = '%s\n%s\n%s' % (method, expires, sig_path) + sig = hmac.new(key, hmac_body, sha1).hexdigest() + self.assert_valid_sig(expires, query_path, [key], sig, prefix='') + def test_obj_odd_chars(self): method = 'GET' expires = int(time() + 86400) @@ -786,6 +816,41 @@ class TestTempURL(unittest.TestCase): self.assertTrue('Temp URL invalid' in resp.body) self.assertTrue('Www-Authenticate' in resp.headers) + def test_no_prefix_match_invalid(self): + method = 'GET' + expires = int(time() + 86400) + sig_path = 'prefix:/v1/a/c/p1/p2/' + query_path = '/v1/a/c/o' + key = 'abc' + hmac_body = '%s\n%s\n%s' % (method, expires, sig_path) + sig = hmac.new(key, hmac_body, sha1).hexdigest() + req = self._make_request( + query_path, keys=[key], + environ={'QUERY_STRING': + 'temp_url_sig=%s&temp_url_expires=%s&temp_url_prefix=%s' % + (sig, expires, 'p1/p2/')}) + resp = req.get_response(self.tempurl) + self.assertEqual(resp.status_int, 401) + self.assertTrue('Temp URL invalid' in resp.body) + self.assertTrue('Www-Authenticate' in resp.headers) + + def test_object_url_with_prefix_invalid(self): + method = 'GET' + expires = int(time() + 86400) + path = '/v1/a/c/o' + key = 'abc' + hmac_body = '%s\n%s\n%s' % (method, expires, path) + sig = hmac.new(key, hmac_body, sha1).hexdigest() + req = self._make_request( + path, keys=[key], + environ={'QUERY_STRING': + 'temp_url_sig=%s&temp_url_expires=%s&temp_url_prefix=o' % + (sig, expires)}) + resp = req.get_response(self.tempurl) + self.assertEqual(resp.status_int, 401) + self.assertTrue('Temp URL invalid' in resp.body) + self.assertTrue('Www-Authenticate' in resp.headers) + def test_disallowed_header_object_manifest(self): self.tempurl = tempurl.filter_factory({})(self.auth) expires = int(time() + 86400) @@ -965,38 +1030,49 @@ class TestTempURL(unittest.TestCase): self.assertTrue('x-conflict-header-test' in resp.headers) self.assertEqual(resp.headers['x-conflict-header-test'], 'value') - def test_get_account_and_container(self): - self.assertEqual(self.tempurl._get_account_and_container({ - 'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/v1/a/c/o'}), ('a', 'c')) - self.assertEqual(self.tempurl._get_account_and_container({ - 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/o'}), ('a', 'c')) - self.assertEqual(self.tempurl._get_account_and_container({ - 'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/v1/a/c/o'}), ('a', 'c')) - self.assertEqual(self.tempurl._get_account_and_container({ - 'REQUEST_METHOD': 'POST', 'PATH_INFO': '/v1/a/c/o'}), ('a', 'c')) - self.assertEqual(self.tempurl._get_account_and_container({ - 'REQUEST_METHOD': 'DELETE', 'PATH_INFO': '/v1/a/c/o'}), ('a', 'c')) - self.assertEqual(self.tempurl._get_account_and_container({ + def test_get_path_parts(self): + self.assertEqual(self.tempurl._get_path_parts({ + 'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/v1/a/c/o'}), + ('a', 'c', 'o')) + self.assertEqual(self.tempurl._get_path_parts({ + 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/o'}), + ('a', 'c', 'o')) + self.assertEqual(self.tempurl._get_path_parts({ + 'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/v1/a/c/o'}), + ('a', 'c', 'o')) + self.assertEqual(self.tempurl._get_path_parts({ + 'REQUEST_METHOD': 'POST', 'PATH_INFO': '/v1/a/c/o'}), + ('a', 'c', 'o')) + self.assertEqual(self.tempurl._get_path_parts({ + 'REQUEST_METHOD': 'DELETE', 'PATH_INFO': '/v1/a/c/o'}), + ('a', 'c', 'o')) + self.assertEqual(self.tempurl._get_path_parts({ 'REQUEST_METHOD': 'UNKNOWN', 'PATH_INFO': '/v1/a/c/o'}), - (None, None)) - self.assertEqual(self.tempurl._get_account_and_container({ - 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/'}), (None, None)) - self.assertEqual(self.tempurl._get_account_and_container({ + (None, None, None)) + self.assertEqual(self.tempurl._get_path_parts({ + 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/'}), + (None, None, None)) + self.assertEqual(self.tempurl._get_path_parts({ 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c//////'}), - (None, None)) - self.assertEqual(self.tempurl._get_account_and_container({ + (None, None, None)) + self.assertEqual(self.tempurl._get_path_parts({ 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c///o///'}), - ('a', 'c')) - self.assertEqual(self.tempurl._get_account_and_container({ - 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c'}), (None, None)) - self.assertEqual(self.tempurl._get_account_and_container({ - 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a//o'}), (None, None)) - self.assertEqual(self.tempurl._get_account_and_container({ - 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1//c/o'}), (None, None)) - self.assertEqual(self.tempurl._get_account_and_container({ - 'REQUEST_METHOD': 'GET', 'PATH_INFO': '//a/c/o'}), (None, None)) - self.assertEqual(self.tempurl._get_account_and_container({ - 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v2/a/c/o'}), (None, None)) + ('a', 'c', '//o///')) + self.assertEqual(self.tempurl._get_path_parts({ + 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c'}), + (None, None, None)) + self.assertEqual(self.tempurl._get_path_parts({ + 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a//o'}), + (None, None, None)) + self.assertEqual(self.tempurl._get_path_parts({ + 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1//c/o'}), + (None, None, None)) + self.assertEqual(self.tempurl._get_path_parts({ + 'REQUEST_METHOD': 'GET', 'PATH_INFO': '//a/c/o'}), + (None, None, None)) + self.assertEqual(self.tempurl._get_path_parts({ + 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v2/a/c/o'}), + (None, None, None)) def test_get_temp_url_info(self): s = 'f5d5051bddf5df7e27c628818738334f' @@ -1005,55 +1081,61 @@ class TestTempURL(unittest.TestCase): self.tempurl._get_temp_url_info( {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % ( s, e)}), - (s, e, None, None)) + (s, e, None, None, None)) + self.assertEqual( + self.tempurl._get_temp_url_info( + {'QUERY_STRING': + 'temp_url_sig=%s&temp_url_expires=%s&temp_url_prefix=%s' % ( + s, e, 'prefix')}), + (s, e, 'prefix', None, None)) self.assertEqual( self.tempurl._get_temp_url_info( {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&' 'filename=bobisyouruncle' % (s, e)}), - (s, e, 'bobisyouruncle', None)) + (s, e, None, 'bobisyouruncle', None)) self.assertEqual( self.tempurl._get_temp_url_info({}), - (None, None, None, None)) + (None, None, None, None, None)) self.assertEqual( self.tempurl._get_temp_url_info( {'QUERY_STRING': 'temp_url_expires=%s' % e}), - (None, e, None, None)) + (None, e, None, None, None)) self.assertEqual( self.tempurl._get_temp_url_info( {'QUERY_STRING': 'temp_url_sig=%s' % s}), - (s, None, None, None)) + (s, None, None, None, None)) self.assertEqual( self.tempurl._get_temp_url_info( {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=bad' % ( s)}), - (s, 0, None, None)) + (s, 0, None, None, None)) self.assertEqual( self.tempurl._get_temp_url_info( {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&' 'inline=' % (s, e)}), - (s, e, None, True)) + (s, e, None, None, True)) self.assertEqual( self.tempurl._get_temp_url_info( {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&' 'filename=bobisyouruncle&inline=' % (s, e)}), - (s, e, 'bobisyouruncle', True)) + (s, e, None, 'bobisyouruncle', True)) e = int(time() - 1) self.assertEqual( self.tempurl._get_temp_url_info( {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % ( s, e)}), - (s, 0, None, None)) + (s, 0, None, None, None)) def test_get_hmacs(self): self.assertEqual( self.tempurl._get_hmacs( - {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/o'}, - 1, [('abc', 'account')]), + {'REQUEST_METHOD': 'GET'}, 1, '/v1/a/c/o', + [('abc', 'account')]), [('026d7f7cc25256450423c7ad03fc9f5ffc1dab6d', 'account')]) self.assertEqual( self.tempurl._get_hmacs( - {'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/v1/a/c/o'}, - 1, [('abc', 'account')], request_method='GET'), + {'REQUEST_METHOD': 'HEAD'}, 1, '/v1/a/c/o', + [('abc', 'account')], request_method='GET'), [('026d7f7cc25256450423c7ad03fc9f5ffc1dab6d', 'account')]) def test_invalid(self):