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:
parent
aea0585ddb
commit
3934bd606a
@ -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
|
||||
|
@ -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
|
||||
----
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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"]
|
||||
|
@ -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'
|
||||
|
Loading…
x
Reference in New Issue
Block a user