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
|
.RS 4
|
||||||
Generates a temporary URL allowing unauthenticated access to the Swift object
|
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
|
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
|
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)
|
should expire. \fBExample\fR: tempurl GET $(date \-d "Jan 1 2016" +%s)
|
||||||
/v1/AUTH_foo/bar_container/quux.md my_secret_tempurl_key \-\-absolute
|
/v1/AUTH_foo/bar_container/quux.md my_secret_tempurl_key \-\-absolute
|
||||||
|
|
||||||
.RE
|
.RE
|
||||||
|
|
||||||
\fBauth\fR
|
\fBauth\fR
|
||||||
|
@ -228,7 +228,7 @@ Capabilities
|
|||||||
Tempurl
|
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
|
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
|
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
|
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 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
|
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
|
Auth
|
||||||
----
|
----
|
||||||
|
@ -1222,9 +1222,8 @@ def st_auth(parser, args, thread_manager):
|
|||||||
print('export OS_AUTH_TOKEN=%s' % sh_quote(token))
|
print('export OS_AUTH_TOKEN=%s' % sh_quote(token))
|
||||||
|
|
||||||
|
|
||||||
st_tempurl_options = '''[--absolute]
|
st_tempurl_options = '''[--absolute] [--prefix-based]
|
||||||
<method> <seconds> <path> <key>
|
<method> <seconds> <path> <key>'''
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
st_tempurl_help = '''
|
st_tempurl_help = '''
|
||||||
@ -1247,6 +1246,7 @@ Optional arguments:
|
|||||||
--absolute Interpret the <seconds> positional argument as a Unix
|
--absolute Interpret the <seconds> positional argument as a Unix
|
||||||
timestamp rather than a number of seconds in the
|
timestamp rather than a number of seconds in the
|
||||||
future.
|
future.
|
||||||
|
--prefix-based If present, a prefix-based tempURL will be generated.
|
||||||
'''.strip('\n')
|
'''.strip('\n')
|
||||||
|
|
||||||
|
|
||||||
@ -1256,8 +1256,14 @@ def st_tempurl(parser, args, thread_manager):
|
|||||||
dest='absolute_expiry', default=False,
|
dest='absolute_expiry', default=False,
|
||||||
help=("If present, seconds argument will be interpreted as a Unix "
|
help=("If present, seconds argument will be interpreted as a Unix "
|
||||||
"timestamp representing when the tempURL should expire, rather "
|
"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)
|
(options, args) = parse_args(parser, args)
|
||||||
args = args[1:]
|
args = args[1:]
|
||||||
if len(args) < 4:
|
if len(args) < 4:
|
||||||
@ -1274,7 +1280,8 @@ def st_tempurl(parser, args, thread_manager):
|
|||||||
method.upper())
|
method.upper())
|
||||||
try:
|
try:
|
||||||
path = generate_temp_url(parsed.path, seconds, key, method,
|
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:
|
except ValueError as err:
|
||||||
thread_manager.error(err)
|
thread_manager.error(err)
|
||||||
return
|
return
|
||||||
|
@ -62,12 +62,14 @@ def prt_bytes(num_bytes, human_flag):
|
|||||||
return '%.1f%s' % (num, suffix)
|
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
|
"""Generates a temporary URL that gives unauthenticated access to the
|
||||||
Swift object.
|
Swift object.
|
||||||
|
|
||||||
:param path: The full path to the Swift object. Example:
|
:param path: The full path to the Swift object or prefix if
|
||||||
/v1/AUTH_account/c/o.
|
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
|
: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
|
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
|
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
|
:param absolute: if True then the seconds parameter is interpreted as an
|
||||||
absolute Unix time, otherwise seconds is interpreted as a relative time
|
absolute Unix time, otherwise seconds is interpreted as a relative time
|
||||||
offset from current 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
|
:raises: ValueError if seconds is not a whole number or path is not to
|
||||||
an object.
|
an object.
|
||||||
:return: the path portion of a temporary URL
|
: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
|
path_for_body = path
|
||||||
|
|
||||||
parts = path_for_body.split('/', 4)
|
parts = path_for_body.split('/', 4)
|
||||||
if len(parts) != 5 or parts[0] or not all(parts[1:]):
|
if len(parts) != 5 or parts[0] or not all(parts[1:(4 if prefix else 5)]):
|
||||||
raise ValueError('path must be full path to an object e.g. /v1/a/c/o')
|
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']
|
standard_methods = ['GET', 'PUT', 'HEAD', 'POST', 'DELETE']
|
||||||
if method.upper() not in standard_methods:
|
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)
|
expiration = int(time.time() + seconds)
|
||||||
else:
|
else:
|
||||||
expiration = seconds
|
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
|
# Encode to UTF-8 for py3 compatibility
|
||||||
if not isinstance(key, six.binary_type):
|
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(
|
temp_url = u'{path}?temp_url_sig={sig}&temp_url_expires={exp}'.format(
|
||||||
path=path_for_body, sig=sig, exp=expiration)
|
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
|
# Have return type match path from caller
|
||||||
if isinstance(path, six.binary_type):
|
if isinstance(path, six.binary_type):
|
||||||
return temp_url.encode('utf-8')
|
return temp_url.encode('utf-8')
|
||||||
|
@ -1549,7 +1549,17 @@ class TestShell(unittest.TestCase):
|
|||||||
"secret_key"]
|
"secret_key"]
|
||||||
swiftclient.shell.main(argv)
|
swiftclient.shell.main(argv)
|
||||||
temp_url.assert_called_with(
|
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='')
|
@mock.patch('swiftclient.shell.generate_temp_url', return_value='')
|
||||||
def test_absolute_expiry_temp_url(self, temp_url):
|
def test_absolute_expiry_temp_url(self, temp_url):
|
||||||
@ -1557,7 +1567,8 @@ class TestShell(unittest.TestCase):
|
|||||||
"secret_key", "--absolute"]
|
"secret_key", "--absolute"]
|
||||||
swiftclient.shell.main(argv)
|
swiftclient.shell.main(argv)
|
||||||
temp_url.assert_called_with(
|
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):
|
def test_temp_url_output(self):
|
||||||
argv = ["", "tempurl", "GET", "60", "/v1/a/c/o",
|
argv = ["", "tempurl", "GET", "60", "/v1/a/c/o",
|
||||||
@ -1575,6 +1586,15 @@ class TestShell(unittest.TestCase):
|
|||||||
expected = "http://saio:8080%s" % expected
|
expected = "http://saio:8080%s" % expected
|
||||||
self.assertEqual(expected, output.out)
|
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):
|
def test_temp_url_error_output(self):
|
||||||
expected = 'path must be full path to an object e.g. /v1/a/c/o\n'
|
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',
|
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 %r but got %r for path %r' %
|
||||||
(expected, output.err, bad_path))
|
(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')
|
@mock.patch('swiftclient.service.Connection')
|
||||||
def test_capabilities(self, connection):
|
def test_capabilities(self, connection):
|
||||||
argv = ["", "capabilities"]
|
argv = ["", "capabilities"]
|
||||||
|
@ -150,6 +150,35 @@ class TestTempURL(unittest.TestCase):
|
|||||||
])
|
])
|
||||||
self.assertIsInstance(url, type(self.url))
|
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):
|
def test_generate_temp_url_invalid_path(self):
|
||||||
with self.assertRaises(ValueError) as exc_manager:
|
with self.assertRaises(ValueError) as exc_manager:
|
||||||
u.generate_temp_url(b'/v1/a/c/\xff', self.seconds, self.key,
|
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],
|
self.assertEqual(exc_manager.exception.args[0],
|
||||||
'path must be full path to an object e.g. /v1/a/c/o')
|
'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):
|
class TestTempURLUnicodePathAndKey(TestTempURL):
|
||||||
url = u'/v1/\u00e4/c/\u00f3'
|
url = u'/v1/\u00e4/c/\u00f3'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user