prefix-based tempurls support

Implements client-side functionality for
prefix-based tempurls.

Please see: https://review.openstack.org/#/c/274048/

Change-Id: I8d7701daee888ed1120271a96c0660b01543ca2d
This commit is contained in:
Christopher Bartz 2016-12-08 13:42:35 +01:00
parent aea0585ddb
commit 3934bd606a
6 changed files with 102 additions and 16 deletions

View File

@ -134,10 +134,12 @@ programs, such as jq.
.RS 4
Generates a temporary URL allowing unauthenticated access to the Swift object
at the given path, using the given HTTP method, for the given number of
seconds, using the given TempURL key. If optional \-\-absolute argument is
seconds, using the given TempURL key. With the optional \-\-prefix\-based option a
prefix-based URL is generated. If optional \-\-absolute argument is
provided, seconds is instead interpreted as a Unix timestamp at which the URL
should expire. \fBExample\fR: tempurl GET $(date \-d "Jan 1 2016" +%s)
/v1/AUTH_foo/bar_container/quux.md my_secret_tempurl_key \-\-absolute
.RE
\fBauth\fR

View File

@ -228,7 +228,7 @@ Capabilities
Tempurl
-------
``tempurl [method] [seconds] [path] [key]``
``tempurl [command-options] [method] [seconds] [path] [key]``
Generates a temporary URL for a Swift object. ``method`` option sets an HTTP method to
allow for this temporary URL that is usually 'GET' or 'PUT'. ``seconds`` option sets
@ -236,7 +236,10 @@ Tempurl
is passed, the Unix timestamp when the temporary URL will expire. ``path`` option sets
the full path to the Swift object. Example: ``/v1/AUTH_account/c/o``. ``key`` option is
the secret temporary URL key set on the Swift cluster. To set a key, run
``swift post -m "Temp-URL-Key: <your secret key>"``.
``swift post -m "Temp-URL-Key: <your secret key>"``. To generate a prefix-based temporary
URL use the ``--prefix-based`` option. This URL will contain the path to the prefix. Do not
forget to append the desired objectname at the end of the path portion (and before the
query portion) before sharing the URL.
Auth
----

View File

@ -1222,9 +1222,8 @@ def st_auth(parser, args, thread_manager):
print('export OS_AUTH_TOKEN=%s' % sh_quote(token))
st_tempurl_options = '''[--absolute]
<method> <seconds> <path> <key>
'''
st_tempurl_options = '''[--absolute] [--prefix-based]
<method> <seconds> <path> <key>'''
st_tempurl_help = '''
@ -1247,6 +1246,7 @@ Optional arguments:
--absolute Interpret the <seconds> positional argument as a Unix
timestamp rather than a number of seconds in the
future.
--prefix-based If present, a prefix-based tempURL will be generated.
'''.strip('\n')
@ -1256,8 +1256,14 @@ def st_tempurl(parser, args, thread_manager):
dest='absolute_expiry', default=False,
help=("If present, seconds argument will be interpreted as a Unix "
"timestamp representing when the tempURL should expire, rather "
"than an offset from the current time")
"than an offset from the current time"),
)
parser.add_argument(
'--prefix-based', action='store_true',
default=False,
help=("If present, a prefix-based tempURL will be generated."),
)
(options, args) = parse_args(parser, args)
args = args[1:]
if len(args) < 4:
@ -1274,7 +1280,8 @@ def st_tempurl(parser, args, thread_manager):
method.upper())
try:
path = generate_temp_url(parsed.path, seconds, key, method,
absolute=options['absolute_expiry'])
absolute=options['absolute_expiry'],
prefix=options['prefix_based'],)
except ValueError as err:
thread_manager.error(err)
return

View File

@ -62,12 +62,14 @@ def prt_bytes(num_bytes, human_flag):
return '%.1f%s' % (num, suffix)
def generate_temp_url(path, seconds, key, method, absolute=False):
def generate_temp_url(path, seconds, key, method, absolute=False,
prefix=False):
"""Generates a temporary URL that gives unauthenticated access to the
Swift object.
:param path: The full path to the Swift object. Example:
/v1/AUTH_account/c/o.
:param path: The full path to the Swift object or prefix if
a prefix-based temporary URL should be generated. Example:
/v1/AUTH_account/c/o or /v1/AUTH_account/c/prefix.
:param seconds: If absolute is False then this specifies the amount of time
in seconds for which the temporary URL will be valid. If absolute is
True then this specifies an absolute time at which the temporary URL
@ -80,6 +82,7 @@ def generate_temp_url(path, seconds, key, method, absolute=False):
:param absolute: if True then the seconds parameter is interpreted as an
absolute Unix time, otherwise seconds is interpreted as a relative time
offset from current time.
:param prefix: if True then a prefix-based temporary URL will be generated.
:raises: ValueError if seconds is not a whole number or path is not to
an object.
:return: the path portion of a temporary URL
@ -103,8 +106,12 @@ def generate_temp_url(path, seconds, key, method, absolute=False):
path_for_body = path
parts = path_for_body.split('/', 4)
if len(parts) != 5 or parts[0] or not all(parts[1:]):
raise ValueError('path must be full path to an object e.g. /v1/a/c/o')
if len(parts) != 5 or parts[0] or not all(parts[1:(4 if prefix else 5)]):
if prefix:
raise ValueError('path must at least contain /v1/a/c/')
else:
raise ValueError('path must be full path to an object'
' e.g. /v1/a/c/o')
standard_methods = ['GET', 'PUT', 'HEAD', 'POST', 'DELETE']
if method.upper() not in standard_methods:
@ -116,7 +123,8 @@ def generate_temp_url(path, seconds, key, method, absolute=False):
expiration = int(time.time() + seconds)
else:
expiration = seconds
hmac_body = u'\n'.join([method.upper(), str(expiration), path_for_body])
hmac_body = u'\n'.join([method.upper(), str(expiration),
('prefix:' if prefix else '') + path_for_body])
# Encode to UTF-8 for py3 compatibility
if not isinstance(key, six.binary_type):
@ -125,6 +133,8 @@ def generate_temp_url(path, seconds, key, method, absolute=False):
temp_url = u'{path}?temp_url_sig={sig}&temp_url_expires={exp}'.format(
path=path_for_body, sig=sig, exp=expiration)
if prefix:
temp_url += u'&temp_url_prefix={}'.format(parts[4])
# Have return type match path from caller
if isinstance(path, six.binary_type):
return temp_url.encode('utf-8')

View File

@ -1549,7 +1549,17 @@ class TestShell(unittest.TestCase):
"secret_key"]
swiftclient.shell.main(argv)
temp_url.assert_called_with(
'/v1/AUTH_account/c/o', "60", 'secret_key', 'GET', absolute=False)
'/v1/AUTH_account/c/o', "60", 'secret_key', 'GET', absolute=False,
prefix=False)
@mock.patch('swiftclient.shell.generate_temp_url', return_value='')
def test_temp_url_prefix_based(self, temp_url):
argv = ["", "tempurl", "GET", "60", "/v1/AUTH_account/c/",
"secret_key", "--prefix-based"]
swiftclient.shell.main(argv)
temp_url.assert_called_with(
'/v1/AUTH_account/c/', "60", 'secret_key', 'GET', absolute=False,
prefix=True)
@mock.patch('swiftclient.shell.generate_temp_url', return_value='')
def test_absolute_expiry_temp_url(self, temp_url):
@ -1557,7 +1567,8 @@ class TestShell(unittest.TestCase):
"secret_key", "--absolute"]
swiftclient.shell.main(argv)
temp_url.assert_called_with(
'/v1/AUTH_account/c/o', "60", 'secret_key', 'GET', absolute=True)
'/v1/AUTH_account/c/o', "60", 'secret_key', 'GET', absolute=True,
prefix=False)
def test_temp_url_output(self):
argv = ["", "tempurl", "GET", "60", "/v1/a/c/o",
@ -1575,6 +1586,15 @@ class TestShell(unittest.TestCase):
expected = "http://saio:8080%s" % expected
self.assertEqual(expected, output.out)
argv = ["", "tempurl", "GET", "60", "/v1/a/c/",
"secret_key", "--absolute", "--prefix"]
with CaptureOutput(suppress_systemexit=True) as output:
swiftclient.shell.main(argv)
sig = '00008c4be1573ba74fc2ab9bce02e3a93d04b349'
expected = ("/v1/a/c/?temp_url_sig=%s&temp_url_expires=60"
"&temp_url_prefix=\n" % sig)
self.assertEqual(expected, output.out)
def test_temp_url_error_output(self):
expected = 'path must be full path to an object e.g. /v1/a/c/o\n'
for bad_path in ('/v1/a/c', 'v1/a/c/o', '/v1/a/c/', '/v1/a//o',
@ -1587,6 +1607,15 @@ class TestShell(unittest.TestCase):
'Expected %r but got %r for path %r' %
(expected, output.err, bad_path))
expected = 'path must at least contain /v1/a/c/\n'
argv = ["", "tempurl", "GET", "60", '/v1/a/c',
"secret_key", "--absolute", '--prefix-based']
with CaptureOutput(suppress_systemexit=True) as output:
swiftclient.shell.main(argv)
self.assertEqual(expected, output.err,
'Expected %r but got %r for path %r' %
(expected, output.err, bad_path))
@mock.patch('swiftclient.service.Connection')
def test_capabilities(self, connection):
argv = ["", "capabilities"]

View File

@ -150,6 +150,35 @@ class TestTempURL(unittest.TestCase):
])
self.assertIsInstance(url, type(self.url))
@mock.patch('hmac.HMAC')
@mock.patch('time.time', return_value=1400000000)
def test_generate_temp_url_prefix(self, time_mock, hmac_mock):
hmac_mock().hexdigest.return_value = 'temp_url_signature'
prefixes = ['', 'o', 'p0/p1/']
for p in prefixes:
hmac_mock.reset_mock()
path = '/v1/AUTH_account/c/' + p
expected_url = path + ('?temp_url_sig=temp_url_signature'
'&temp_url_expires=1400003600'
'&temp_url_prefix=' + p)
expected_body = '\n'.join([
self.method,
'1400003600',
'prefix:' + path,
]).encode('utf-8')
url = u.generate_temp_url(path, self.seconds,
self.key, self.method, prefix=True)
key = self.key
if not isinstance(key, six.binary_type):
key = key.encode('utf-8')
self.assertEqual(url, expected_url)
self.assertEqual(hmac_mock.mock_calls, [
mock.call(key, expected_body, sha1),
mock.call().hexdigest(),
])
self.assertIsInstance(url, type(path))
def test_generate_temp_url_invalid_path(self):
with self.assertRaises(ValueError) as exc_manager:
u.generate_temp_url(b'/v1/a/c/\xff', self.seconds, self.key,
@ -221,6 +250,12 @@ class TestTempURL(unittest.TestCase):
self.assertEqual(exc_manager.exception.args[0],
'path must be full path to an object e.g. /v1/a/c/o')
with self.assertRaises(ValueError) as exc_manager:
u.generate_temp_url('/v1/a/c', 60, self.key, self.method,
prefix=True)
self.assertEqual(exc_manager.exception.args[0],
'path must at least contain /v1/a/c/')
class TestTempURLUnicodePathAndKey(TestTempURL):
url = u'/v1/\u00e4/c/\u00f3'