Merge "tempurls with a prefix-based scope"

This commit is contained in:
Jenkins 2016-12-13 01:58:16 +00:00 committed by Gerrit Code Review
commit 54d1e1bae8
4 changed files with 321 additions and 102 deletions

View File

@ -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 downloads the object directly from Object Storage, eliminating the need
for the website to act as a proxy for the request. 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 Ask your cloud administrator to enable the temporary URL feature. For
information, see :ref:`tempurl` in the *Source Documentation*. 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 response header. Browsers can interpret this file name value as a file
attachment to be saved. 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:
Secret Keys Secret Keys
@ -114,17 +129,16 @@ signature includes these elements:
into the future. into the future.
- The path. Starting with ``/v1/`` onwards and including a container - The path. Starting with ``/v1/`` onwards and including a container
name and object. In the example below, the path is name and object. The path for prefix-based signatures must start with
``/v1/my_account/container/object``. Do not URL-encode the path at ``prefix:/v1/``. Do not URL-encode the path at this stage.
this stage.
- The secret key. Use one of the key values as described - The secret key. Use one of the key values as described
in :ref:`secret_keys`. 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: temporary URLs:
**Example HMAC-SHA1 signature for temporary URLs** **Example HMAC-SHA1 signature for object-based temporary URLs**
.. code:: .. code::
@ -139,6 +153,21 @@ temporary URLs:
hmac_body = '%s\n%s\n%s' % (method, expires, path) hmac_body = '%s\n%s\n%s' % (method, expires, path)
signature = hmac.new(key, hmac_body, sha1).hexdigest() 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. Do not URL-encode the path when you generate the HMAC-SHA1 signature.
However, when you make the actual HTTP request, you should properly However, when you make the actual HTTP request, you should properly

View File

@ -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 accidentally post it on a forum, etc. the direct access would be
limited to the expiration time set when the website created the link. 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 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) 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``, signature is generated using the HTTP method to allow (``GET``, ``PUT``,
``DELETE``, etc.), the Unix timestamp the access should be allowed until, ``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_sig=da39a3ee5e6b4b0d3255bfef95601890afd80709&
temp_url_expires=1323479485 temp_url_expires=1323479485
Any alteration of the resource path or query arguments would result in If a prefix-based signature with the prefix ``pre`` is desired, set path to::
``401 Unauthorized``. Similarly, a ``PUT`` where ``GET`` was the allowed method
would be rejected with ``401 Unauthorized``. However, ``HEAD`` is allowed if path = 'prefix:/v1/AUTH_account/container/pre'
``GET``, ``PUT``, or ``POST`` is allowed.
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 Using this in combination with browser form post translation
middleware could also allow direct-from-browser uploads to specific middleware could also allow direct-from-browser uploads to specific
@ -357,28 +384,36 @@ class TempURL(object):
if env['REQUEST_METHOD'] == 'OPTIONS': if env['REQUEST_METHOD'] == 'OPTIONS':
return self.app(env, start_response) return self.app(env, start_response)
info = self._get_temp_url_info(env) 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: if temp_url_sig is None and temp_url_expires is None:
return self.app(env, start_response) return self.app(env, start_response)
if not temp_url_sig or not temp_url_expires: if not temp_url_sig or not temp_url_expires:
return self._invalid(env, start_response) 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: if not account:
return self._invalid(env, start_response) return self._invalid(env, start_response)
keys = self._get_keys(env) keys = self._get_keys(env)
if not keys: if not keys:
return self._invalid(env, start_response) 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': if env['REQUEST_METHOD'] == 'HEAD':
hmac_vals = ( hmac_vals = (
self._get_hmacs(env, temp_url_expires, keys) + self._get_hmacs(env, temp_url_expires, path, keys) +
self._get_hmacs(env, temp_url_expires, keys, self._get_hmacs(env, temp_url_expires, path, keys,
request_method='GET') + request_method='GET') +
self._get_hmacs(env, temp_url_expires, keys, self._get_hmacs(env, temp_url_expires, path, keys,
request_method='POST') + request_method='POST') +
self._get_hmacs(env, temp_url_expires, keys, self._get_hmacs(env, temp_url_expires, path, keys,
request_method='PUT')) request_method='PUT'))
else: 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 is_valid_hmac = False
hmac_scope = None hmac_scope = None
@ -410,6 +445,8 @@ class TempURL(object):
env['REMOTE_USER'] = '.wsgi.tempurl' env['REMOTE_USER'] = '.wsgi.tempurl'
qs = {'temp_url_sig': temp_url_sig, qs = {'temp_url_sig': temp_url_sig,
'temp_url_expires': temp_url_expires} 'temp_url_expires': temp_url_expires}
if temp_url_prefix is not None:
qs['temp_url_prefix'] = temp_url_prefix
if filename: if filename:
qs['filename'] = filename qs['filename'] = filename
env['QUERY_STRING'] = urlencode(qs) env['QUERY_STRING'] = urlencode(qs)
@ -457,36 +494,38 @@ class TempURL(object):
return self.app(env, _start_response) 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 Return the account, container and object name for the request,
object request and one of the configured methods; otherwise, None is if it's an object request and one of the configured methods;
returned. otherwise, None is returned.
:param env: The WSGI environment for the request. :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']: if env['REQUEST_METHOD'] in self.conf['methods']:
try: try:
ver, acc, cont, obj = split_path(env['PATH_INFO'], 4, 4, True) ver, acc, cont, obj = split_path(env['PATH_INFO'], 4, 4, True)
except ValueError: except ValueError:
return (None, None) return (None, None, None)
if ver == 'v1' and obj.strip('/'): if ver == 'v1' and obj.strip('/'):
return (acc, cont) return (acc, cont, obj)
return (None, None) return (None, None, None)
def _get_temp_url_info(self, env): def _get_temp_url_info(self, env):
""" """
Returns the provided temporary URL parameters (sig, expires), Returns the provided temporary URL parameters (sig, expires, prefix),
if given and syntactically valid. Either sig or expires could if given and syntactically valid. Either sig, expires or prefix could
be None if not provided. If provided, expires is also be None if not provided. If provided, expires is also
converted to an int if possible or 0 if not, and checked for converted to an int if possible or 0 if not, and checked for
expiration (returns 0 if expired). expiration (returns 0 if expired).
:param env: The WSGI environment for the request. :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) qs = parse_qs(env.get('QUERY_STRING', ''), keep_blank_values=True)
if 'temp_url_sig' in qs: if 'temp_url_sig' in qs:
temp_url_sig = qs['temp_url_sig'][0] temp_url_sig = qs['temp_url_sig'][0]
@ -497,11 +536,14 @@ class TempURL(object):
temp_url_expires = 0 temp_url_expires = 0
if temp_url_expires < time(): if temp_url_expires < time():
temp_url_expires = 0 temp_url_expires = 0
if 'temp_url_prefix' in qs:
temp_url_prefix = qs['temp_url_prefix'][0]
if 'filename' in qs: if 'filename' in qs:
filename = qs['filename'][0] filename = qs['filename'][0]
if 'inline' in qs: if 'inline' in qs:
inline = True 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): def _get_keys(self, env):
""" """
@ -531,11 +573,13 @@ class TempURL(object):
return ([(ak, ACCOUNT_SCOPE) for ak in account_keys] + return ([(ak, ACCOUNT_SCOPE) for ak in account_keys] +
[(ck, CONTAINER_SCOPE) for ck in container_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 env: The WSGI environment for the request.
:param expires: Unix timestamp as an int for when the URL :param expires: Unix timestamp as an int for when the URL
expires. expires.
:param path: The path which is used for hashing.
:param scoped_keys: (key, scope) tuples like _get_keys() returns :param scoped_keys: (key, scope) tuples like _get_keys() returns
:param request_method: Optional override of the request in :param request_method: Optional override of the request in
the WSGI env. For example, if a HEAD the WSGI env. For example, if a HEAD
@ -547,8 +591,9 @@ class TempURL(object):
""" """
if not request_method: if not request_method:
request_method = env['REQUEST_METHOD'] request_method = env['REQUEST_METHOD']
return [ 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] for (key, scope) in scoped_keys]
def _invalid(self, env, start_response): def _invalid(self, env, start_response):

View File

@ -87,17 +87,16 @@ class TestTempurl(Base):
(self.env.tempurl_enabled,)) (self.env.tempurl_enabled,))
expires = int(time.time()) + 86400 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), 'GET', expires, self.env.conn.make_path(self.env.obj.path),
self.env.tempurl_key) 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): def tempurl_parms(self, method, expires, path, key):
return hmac.new( sig = hmac.new(
key, key,
'%s\n%s\n%s' % (method, expires, urllib.parse.unquote(path)), '%s\n%s\n%s' % (method, expires, urllib.parse.unquote(path)),
hashlib.sha1).hexdigest() hashlib.sha1).hexdigest()
return {'temp_url_sig': sig, 'temp_url_expires': str(expires)}
def test_GET(self): def test_GET(self):
contents = self.env.obj.read( contents = self.env.obj.read(
@ -111,11 +110,9 @@ class TestTempurl(Base):
def test_GET_with_key_2(self): def test_GET_with_key_2(self):
expires = int(time.time()) + 86400 expires = int(time.time()) + 86400
sig = self.tempurl_sig( parms = self.tempurl_parms(
'GET', expires, self.env.conn.make_path(self.env.obj.path), 'GET', expires, self.env.conn.make_path(self.env.obj.path),
self.env.tempurl_key2) 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}) contents = self.env.obj.read(parms=parms, cfg={'no_auth_token': True})
self.assertEqual(contents, "obj contents") self.assertEqual(contents, "obj contents")
@ -135,11 +132,9 @@ class TestTempurl(Base):
(self.env.container.name,)}) (self.env.container.name,)})
expires = int(time.time()) + 86400 expires = int(time.time()) + 86400
sig = self.tempurl_sig( parms = self.tempurl_parms(
'GET', expires, self.env.conn.make_path(manifest.path), 'GET', expires, self.env.conn.make_path(manifest.path),
self.env.tempurl_key) self.env.tempurl_key)
parms = {'temp_url_sig': sig,
'temp_url_expires': str(expires)}
contents = manifest.read(parms=parms, cfg={'no_auth_token': True}) contents = manifest.read(parms=parms, cfg={'no_auth_token': True})
self.assertEqual(contents, "one fish two fish red fish blue fish") self.assertEqual(contents, "one fish two fish red fish blue fish")
@ -162,11 +157,9 @@ class TestTempurl(Base):
(self.env.container.name,)}) (self.env.container.name,)})
expires = int(time.time()) + 86400 expires = int(time.time()) + 86400
sig = self.tempurl_sig( parms = self.tempurl_parms(
'GET', expires, self.env.conn.make_path(manifest.path), 'GET', expires, self.env.conn.make_path(manifest.path),
self.env.tempurl_key) self.env.tempurl_key)
parms = {'temp_url_sig': sig,
'temp_url_expires': str(expires)}
# cross container tempurl works fine for account tempurl key # cross container tempurl works fine for account tempurl key
contents = manifest.read(parms=parms, cfg={'no_auth_token': True}) 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()) new_obj = self.env.container.file(Utils.create_name())
expires = int(time.time()) + 86400 expires = int(time.time()) + 86400
sig = self.tempurl_sig( put_parms = self.tempurl_parms(
'PUT', expires, self.env.conn.make_path(new_obj.path), 'PUT', expires, self.env.conn.make_path(new_obj.path),
self.env.tempurl_key) self.env.tempurl_key)
put_parms = {'temp_url_sig': sig,
'temp_url_expires': str(expires)}
new_obj.write('new obj contents', new_obj.write('new obj contents',
parms=put_parms, cfg={'no_auth_token': True}) 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 # give out a signature which allows a PUT to new_obj
expires = int(time.time()) + 86400 expires = int(time.time()) + 86400
sig = self.tempurl_sig( put_parms = self.tempurl_parms(
'PUT', expires, self.env.conn.make_path(new_obj.path), 'PUT', expires, self.env.conn.make_path(new_obj.path),
self.env.tempurl_key) 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 to create manifest pointing to some random container
try: try:
@ -230,11 +219,9 @@ class TestTempurl(Base):
# try again using a tempurl POST to an already created object # try again using a tempurl POST to an already created object
new_obj.write('', {}, parms=put_parms, cfg={'no_auth_token': True}) new_obj.write('', {}, parms=put_parms, cfg={'no_auth_token': True})
expires = int(time.time()) + 86400 expires = int(time.time()) + 86400
sig = self.tempurl_sig( post_parms = self.tempurl_parms(
'POST', expires, self.env.conn.make_path(new_obj.path), 'POST', expires, self.env.conn.make_path(new_obj.path),
self.env.tempurl_key) self.env.tempurl_key)
post_parms = {'temp_url_sig': sig,
'temp_url_expires': str(expires)}
try: try:
new_obj.post({'x-object-manifest': '%s/foo' % other_container}, new_obj.post({'x-object-manifest': '%s/foo' % other_container},
parms=post_parms, cfg={'no_auth_token': True}) parms=post_parms, cfg={'no_auth_token': True})
@ -245,11 +232,9 @@ class TestTempurl(Base):
def test_HEAD(self): def test_HEAD(self):
expires = int(time.time()) + 86400 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), 'HEAD', expires, self.env.conn.make_path(self.env.obj.path),
self.env.tempurl_key) self.env.tempurl_key)
head_parms = {'temp_url_sig': sig,
'temp_url_expires': str(expires)}
self.assertTrue(self.env.obj.info(parms=head_parms, self.assertTrue(self.env.obj.info(parms=head_parms,
cfg={'no_auth_token': True})) cfg={'no_auth_token': True}))
@ -312,6 +297,84 @@ class TestTempurl(Base):
self.assert_status([401]) 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): class TestTempurlUTF8(Base2, TestTempurl):
set_up = False set_up = False

View File

@ -80,7 +80,7 @@ class TestTempURL(unittest.TestCase):
if environ is None: if environ is None:
environ = {} 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, self._fake_cache_environ(environ, account, keys,
container_keys=container_keys) container_keys=container_keys)
req = Request.blank(path, environ=environ, **kwargs) req = Request.blank(path, environ=environ, **kwargs)
@ -125,11 +125,14 @@ class TestTempURL(unittest.TestCase):
environ={'REQUEST_METHOD': 'OPTIONS'}).get_response(self.tempurl) environ={'REQUEST_METHOD': 'OPTIONS'}).get_response(self.tempurl)
self.assertEqual(resp.status_int, 200) 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: if not environ:
environ = {} environ = {}
environ['QUERY_STRING'] = 'temp_url_sig=%s&temp_url_expires=%s' % ( environ['QUERY_STRING'] = 'temp_url_sig=%s&temp_url_expires=%s' % (
sig, expires) sig, expires)
if prefix is not None:
environ['QUERY_STRING'] += '&temp_url_prefix=%s' % prefix
req = self._make_request(path, keys=keys, environ=environ) req = self._make_request(path, keys=keys, environ=environ)
self.tempurl.app = FakeApp(iter([('200 Ok', (), '123')])) self.tempurl.app = FakeApp(iter([('200 Ok', (), '123')]))
resp = req.get_response(self.tempurl) 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['swift.authorize_override'], True)
self.assertEqual(req.environ['REMOTE_USER'], '.wsgi.tempurl') 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): def test_obj_odd_chars(self):
method = 'GET' method = 'GET'
expires = int(time() + 86400) expires = int(time() + 86400)
@ -786,6 +816,41 @@ class TestTempURL(unittest.TestCase):
self.assertTrue('Temp URL invalid' in resp.body) self.assertTrue('Temp URL invalid' in resp.body)
self.assertTrue('Www-Authenticate' in resp.headers) 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): def test_disallowed_header_object_manifest(self):
self.tempurl = tempurl.filter_factory({})(self.auth) self.tempurl = tempurl.filter_factory({})(self.auth)
expires = int(time() + 86400) expires = int(time() + 86400)
@ -965,38 +1030,49 @@ class TestTempURL(unittest.TestCase):
self.assertTrue('x-conflict-header-test' in resp.headers) self.assertTrue('x-conflict-header-test' in resp.headers)
self.assertEqual(resp.headers['x-conflict-header-test'], 'value') self.assertEqual(resp.headers['x-conflict-header-test'], 'value')
def test_get_account_and_container(self): def test_get_path_parts(self):
self.assertEqual(self.tempurl._get_account_and_container({ self.assertEqual(self.tempurl._get_path_parts({
'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/v1/a/c/o'}), ('a', 'c')) 'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/v1/a/c/o'}),
self.assertEqual(self.tempurl._get_account_and_container({ ('a', 'c', 'o'))
'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/o'}), ('a', 'c')) self.assertEqual(self.tempurl._get_path_parts({
self.assertEqual(self.tempurl._get_account_and_container({ 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/o'}),
'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/v1/a/c/o'}), ('a', 'c')) ('a', 'c', 'o'))
self.assertEqual(self.tempurl._get_account_and_container({ self.assertEqual(self.tempurl._get_path_parts({
'REQUEST_METHOD': 'POST', 'PATH_INFO': '/v1/a/c/o'}), ('a', 'c')) 'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/v1/a/c/o'}),
self.assertEqual(self.tempurl._get_account_and_container({ ('a', 'c', 'o'))
'REQUEST_METHOD': 'DELETE', 'PATH_INFO': '/v1/a/c/o'}), ('a', 'c')) self.assertEqual(self.tempurl._get_path_parts({
self.assertEqual(self.tempurl._get_account_and_container({ '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'}), 'REQUEST_METHOD': 'UNKNOWN', 'PATH_INFO': '/v1/a/c/o'}),
(None, None)) (None, None, None))
self.assertEqual(self.tempurl._get_account_and_container({ self.assertEqual(self.tempurl._get_path_parts({
'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/'}), (None, None)) 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/'}),
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//////'}), 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c//////'}),
(None, None)) (None, None, None))
self.assertEqual(self.tempurl._get_account_and_container({ self.assertEqual(self.tempurl._get_path_parts({
'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c///o///'}), 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c///o///'}),
('a', 'c')) ('a', 'c', '//o///'))
self.assertEqual(self.tempurl._get_account_and_container({ self.assertEqual(self.tempurl._get_path_parts({
'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c'}), (None, None)) 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c'}),
self.assertEqual(self.tempurl._get_account_and_container({ (None, None, None))
'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a//o'}), (None, None)) self.assertEqual(self.tempurl._get_path_parts({
self.assertEqual(self.tempurl._get_account_and_container({ 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a//o'}),
'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1//c/o'}), (None, None)) (None, None, None))
self.assertEqual(self.tempurl._get_account_and_container({ self.assertEqual(self.tempurl._get_path_parts({
'REQUEST_METHOD': 'GET', 'PATH_INFO': '//a/c/o'}), (None, None)) 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1//c/o'}),
self.assertEqual(self.tempurl._get_account_and_container({ (None, None, None))
'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v2/a/c/o'}), (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): def test_get_temp_url_info(self):
s = 'f5d5051bddf5df7e27c628818738334f' s = 'f5d5051bddf5df7e27c628818738334f'
@ -1005,55 +1081,61 @@ class TestTempURL(unittest.TestCase):
self.tempurl._get_temp_url_info( self.tempurl._get_temp_url_info(
{'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % ( {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (
s, e)}), 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.assertEqual(
self.tempurl._get_temp_url_info( self.tempurl._get_temp_url_info(
{'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&' {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&'
'filename=bobisyouruncle' % (s, e)}), 'filename=bobisyouruncle' % (s, e)}),
(s, e, 'bobisyouruncle', None)) (s, e, None, 'bobisyouruncle', None))
self.assertEqual( self.assertEqual(
self.tempurl._get_temp_url_info({}), self.tempurl._get_temp_url_info({}),
(None, None, None, None)) (None, None, None, None, None))
self.assertEqual( self.assertEqual(
self.tempurl._get_temp_url_info( self.tempurl._get_temp_url_info(
{'QUERY_STRING': 'temp_url_expires=%s' % e}), {'QUERY_STRING': 'temp_url_expires=%s' % e}),
(None, e, None, None)) (None, e, None, None, None))
self.assertEqual( self.assertEqual(
self.tempurl._get_temp_url_info( self.tempurl._get_temp_url_info(
{'QUERY_STRING': 'temp_url_sig=%s' % s}), {'QUERY_STRING': 'temp_url_sig=%s' % s}),
(s, None, None, None)) (s, None, None, None, None))
self.assertEqual( self.assertEqual(
self.tempurl._get_temp_url_info( self.tempurl._get_temp_url_info(
{'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=bad' % ( {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=bad' % (
s)}), s)}),
(s, 0, None, None)) (s, 0, None, None, None))
self.assertEqual( self.assertEqual(
self.tempurl._get_temp_url_info( self.tempurl._get_temp_url_info(
{'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&' {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&'
'inline=' % (s, e)}), 'inline=' % (s, e)}),
(s, e, None, True)) (s, e, None, None, True))
self.assertEqual( self.assertEqual(
self.tempurl._get_temp_url_info( self.tempurl._get_temp_url_info(
{'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&' {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&'
'filename=bobisyouruncle&inline=' % (s, e)}), 'filename=bobisyouruncle&inline=' % (s, e)}),
(s, e, 'bobisyouruncle', True)) (s, e, None, 'bobisyouruncle', True))
e = int(time() - 1) e = int(time() - 1)
self.assertEqual( self.assertEqual(
self.tempurl._get_temp_url_info( self.tempurl._get_temp_url_info(
{'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % ( {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (
s, e)}), s, e)}),
(s, 0, None, None)) (s, 0, None, None, None))
def test_get_hmacs(self): def test_get_hmacs(self):
self.assertEqual( self.assertEqual(
self.tempurl._get_hmacs( self.tempurl._get_hmacs(
{'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/o'}, {'REQUEST_METHOD': 'GET'}, 1, '/v1/a/c/o',
1, [('abc', 'account')]), [('abc', 'account')]),
[('026d7f7cc25256450423c7ad03fc9f5ffc1dab6d', 'account')]) [('026d7f7cc25256450423c7ad03fc9f5ffc1dab6d', 'account')])
self.assertEqual( self.assertEqual(
self.tempurl._get_hmacs( self.tempurl._get_hmacs(
{'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/v1/a/c/o'}, {'REQUEST_METHOD': 'HEAD'}, 1, '/v1/a/c/o',
1, [('abc', 'account')], request_method='GET'), [('abc', 'account')], request_method='GET'),
[('026d7f7cc25256450423c7ad03fc9f5ffc1dab6d', 'account')]) [('026d7f7cc25256450423c7ad03fc9f5ffc1dab6d', 'account')])
def test_invalid(self): def test_invalid(self):