From def0e0a6435deee5c55b7859e1b132590ea0860c Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Wed, 25 Jun 2014 13:28:42 -0700 Subject: Adding Swift Temporary URL support Temporary URLs allow a user to sign an object URL with a shared secret to so that the object can be downloaded without auth for a specified amount of time. http://docs.openstack.org/trunk/config-reference/content/object-storage-tempurl.html Change-Id: Ife0b6c98c975e074d4dad0a31145573b784747c5 --- swiftclient/shell.py | 48 +++++++++++++++++++++++++++++++++++++++++++++-- swiftclient/utils.py | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ tests/unit/test_shell.py | 11 +++++++++++ tests/unit/test_utils.py | 39 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 2 deletions(-) diff --git a/swiftclient/shell.py b/swiftclient/shell.py index d035388..12eeb63 100755 --- a/swiftclient/shell.py +++ b/swiftclient/shell.py @@ -38,7 +38,7 @@ except ImportError: from swiftclient import Connection, RequestException from swiftclient import command_helpers -from swiftclient.utils import config_true_value, prt_bytes +from swiftclient.utils import config_true_value, prt_bytes, generate_temp_url from swiftclient.multithreading import MultiThreadingManager from swiftclient.exceptions import ClientException from swiftclient import __version__ as client_version @@ -1240,6 +1240,45 @@ def st_capabilities(parser, args, thread_manager): st_info = st_capabilities +st_tempurl_options = ' ' + +st_tempurl_help = ''' +Generates a temporary URL for a Swift object. + +Positions arguments: + [method] An HTTP method to allow for this temporary URL. + Usually 'GET' or 'PUT'. + [seconds] The amount of time in seconds the temporary URL will + be valid for. + [path] The full path to the Swift object. Example: + /v1/AUTH_account/c/o. + [key] The secret temporary URL key set on the Swift cluster. + To set a key, run \'swift post -m + "Temp-URL-Key:b3968d0207b54ece87cccc06515a89d4"\' +'''.strip('\n') + + +def st_tempurl(parser, args, thread_manager): + (options, args) = parse_args(parser, args) + args = args[1:] + if len(args) < 4: + thread_manager.error('Usage: %s tempurl %s\n%s', BASENAME, + st_tempurl_options, st_tempurl_help) + return + method, seconds, path, key = args[:4] + try: + seconds = int(seconds) + except ValueError: + thread_manager.error('Seconds must be an integer') + return + if method.upper() not in ['GET', 'PUT', 'HEAD', 'POST', 'DELETE']: + thread_manager.print_msg('WARNING: Non default HTTP method %s for ' + 'tempurl specified, possibly an error' % + method.upper()) + url = generate_temp_url(path, seconds, key, method) + thread_manager.print_msg(url) + + def split_headers(options, prefix='', thread_manager=None): """ Splits 'Key: Value' strings and returns them as a dictionary. @@ -1269,6 +1308,10 @@ def parse_args(parser, args, enforce_requires=True): args = ['-h'] (options, args) = parser.parse_args(args) + # Short circuit for tempurl, which doesn't need auth + if len(args) > 0 and args[0] == 'tempurl': + return options, args + if (not (options.auth and options.user and options.key)): # Use 2.0 auth if none of the old args are present options.auth_version = '2.0' @@ -1351,6 +1394,7 @@ Positional arguments: or object. upload Uploads files or directories to the given container. capabilities List cluster capabilities. + tempurl Create a temporary URL Examples: @@ -1488,7 +1532,7 @@ Examples: parser.enable_interspersed_args() commands = ('delete', 'download', 'list', 'post', - 'stat', 'upload', 'capabilities', 'info') + 'stat', 'upload', 'capabilities', 'info', 'tempurl') if not args or args[0] not in commands: parser.print_usage() if args: diff --git a/swiftclient/utils.py b/swiftclient/utils.py index 058181d..0f442b3 100644 --- a/swiftclient/utils.py +++ b/swiftclient/utils.py @@ -13,6 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. """Miscellaneous utility functions for use with Swift.""" +import hashlib +import hmac +import logging +import time import six @@ -59,6 +63,51 @@ def prt_bytes(bytes, human_flag): return(bytes) +def generate_temp_url(path, seconds, key, method): + """ 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 seconds: The amount of time in seconds the temporary URL will + be valid for. + :param key: The secret temporary URL key set on the Swift cluster. + To set a key, run 'swift post -m + "Temp-URL-Key:b3968d0207b54ece87cccc06515a89d4"' + :param method: A HTTP method, typically either GET or PUT, to allow for + this temporary URL. + :raises: ValueError if seconds is not a positive integer + :raises: TypeError if seconds is not an integer + :return: the path portion of a temporary URL + """ + if seconds < 0: + raise ValueError('seconds must be a positive integer') + try: + expiration = int(time.time() + seconds) + except TypeError: + raise TypeError('seconds must be an integer') + + standard_methods = ['GET', 'PUT', 'HEAD', 'POST', 'DELETE'] + if method.upper() not in standard_methods: + logger = logging.getLogger("swiftclient") + logger.warning('Non default HTTP method %s for tempurl specified, ' + 'possibly an error', method.upper()) + + hmac_body = '\n'.join([method.upper(), str(expiration), path]) + + # Encode to UTF-8 for py3 compatibility + sig = hmac.new(key.encode(), + hmac_body.encode(), + hashlib.sha1).hexdigest() + + return ('{path}?temp_url_sig=' + '{sig}&temp_url_expires={exp}'.format( + path=path, + sig=sig, + exp=expiration) + ) + + class LengthWrapper(object): def __init__(self, readable, length): diff --git a/tests/unit/test_shell.py b/tests/unit/test_shell.py index 0c28297..33cc91a 100644 --- a/tests/unit/test_shell.py +++ b/tests/unit/test_shell.py @@ -22,6 +22,7 @@ import six import swiftclient import swiftclient.shell +import swiftclient.utils if six.PY2: @@ -328,6 +329,16 @@ class TestShell(unittest.TestCase): 'Content-Type': 'text/plain', 'X-Object-Meta-Color': 'Blue'}) + @mock.patch('swiftclient.shell.generate_temp_url') + def test_temp_url(self, temp_url): + argv = ["", "tempurl", "GET", "60", "/v1/AUTH_account/c/o", + "secret_key" + ] + temp_url.return_value = "" + swiftclient.shell.main(argv) + temp_url.assert_called_with( + '/v1/AUTH_account/c/o', 60, 'secret_key', 'GET') + @mock.patch('swiftclient.shell.Connection') def test_capabilities(self, connection): argv = ["", "capabilities"] diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index d9d74c5..f072aed 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -15,6 +15,7 @@ import testtools +import mock import six import tempfile @@ -122,6 +123,44 @@ class TestPrtBytes(testtools.TestCase): self.assertEqual('1024Y', u.prt_bytes(bytes_, True).lstrip()) +class TestTempURL(testtools.TestCase): + + def setUp(self): + super(TestTempURL, self).setUp() + self.url = '/v1/AUTH_account/c/o' + self.seconds = 3600 + self.key = 'correcthorsebatterystaple' + self.method = 'GET' + + @mock.patch('hmac.HMAC.hexdigest') + @mock.patch('time.time') + def test_generate_temp_url(self, time_mock, hmac_mock): + time_mock.return_value = 1400000000 + hmac_mock.return_value = 'temp_url_signature' + expected_url = ( + '/v1/AUTH_account/c/o?' + 'temp_url_sig=temp_url_signature&' + 'temp_url_expires=1400003600') + url = u.generate_temp_url(self.url, self.seconds, self.key, + self.method) + self.assertEqual(url, expected_url) + + def test_generate_temp_url_bad_seconds(self): + self.assertRaises(TypeError, + u.generate_temp_url, + self.url, + 'not_an_int', + self.key, + self.method) + + self.assertRaises(ValueError, + u.generate_temp_url, + self.url, + -1, + self.key, + self.method) + + class TestLengthWrapper(testtools.TestCase): def test_stringio(self): -- cgit v1.2.1