From 3934bd606acc2333ee9ae63a40baa35928ef908d Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Thu, 8 Dec 2016 13:42:35 +0100 Subject: [PATCH] prefix-based tempurls support Implements client-side functionality for prefix-based tempurls. Please see: https://review.openstack.org/#/c/274048/ Change-Id: I8d7701daee888ed1120271a96c0660b01543ca2d --- doc/manpages/swift.1 | 4 +++- doc/source/cli.rst | 7 +++++-- swiftclient/shell.py | 17 ++++++++++++----- swiftclient/utils.py | 22 ++++++++++++++++------ tests/unit/test_shell.py | 33 +++++++++++++++++++++++++++++++-- tests/unit/test_utils.py | 35 +++++++++++++++++++++++++++++++++++ 6 files changed, 102 insertions(+), 16 deletions(-) diff --git a/doc/manpages/swift.1 b/doc/manpages/swift.1 index 5d21c187..b65170c4 100644 --- a/doc/manpages/swift.1 +++ b/doc/manpages/swift.1 @@ -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 diff --git a/doc/source/cli.rst b/doc/source/cli.rst index 6c58d112..1f76b054 100644 --- a/doc/source/cli.rst +++ b/doc/source/cli.rst @@ -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: "``. + ``swift post -m "Temp-URL-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 ---- diff --git a/swiftclient/shell.py b/swiftclient/shell.py index 9819bf35..6956a159 100755 --- a/swiftclient/shell.py +++ b/swiftclient/shell.py @@ -1222,9 +1222,8 @@ def st_auth(parser, args, thread_manager): print('export OS_AUTH_TOKEN=%s' % sh_quote(token)) -st_tempurl_options = '''[--absolute] - -''' +st_tempurl_options = '''[--absolute] [--prefix-based] + ''' st_tempurl_help = ''' @@ -1247,6 +1246,7 @@ Optional arguments: --absolute Interpret the 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 diff --git a/swiftclient/utils.py b/swiftclient/utils.py index e14602dc..5ba6d5b2 100644 --- a/swiftclient/utils.py +++ b/swiftclient/utils.py @@ -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') diff --git a/tests/unit/test_shell.py b/tests/unit/test_shell.py index 00677dfe..f7e9c1ab 100644 --- a/tests/unit/test_shell.py +++ b/tests/unit/test_shell.py @@ -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"] diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 787f645b..cbea8d86 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -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'