Skip to content

Commit def0e0a

Browse files
author
Josh Gachnang
committed
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
1 parent 3d0de79 commit def0e0a

File tree

4 files changed

+145
-2
lines changed

4 files changed

+145
-2
lines changed

swiftclient/shell.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838

3939
from swiftclient import Connection, RequestException
4040
from swiftclient import command_helpers
41-
from swiftclient.utils import config_true_value, prt_bytes
41+
from swiftclient.utils import config_true_value, prt_bytes, generate_temp_url
4242
from swiftclient.multithreading import MultiThreadingManager
4343
from swiftclient.exceptions import ClientException
4444
from swiftclient import __version__ as client_version
@@ -1240,6 +1240,45 @@ def _print_compo_cap(name, capabilities):
12401240
st_info = st_capabilities
12411241

12421242

1243+
st_tempurl_options = '<method> <seconds> <path> <key>'
1244+
1245+
st_tempurl_help = '''
1246+
Generates a temporary URL for a Swift object.
1247+
1248+
Positions arguments:
1249+
[method] An HTTP method to allow for this temporary URL.
1250+
Usually 'GET' or 'PUT'.
1251+
[seconds] The amount of time in seconds the temporary URL will
1252+
be valid for.
1253+
[path] The full path to the Swift object. Example:
1254+
/v1/AUTH_account/c/o.
1255+
[key] The secret temporary URL key set on the Swift cluster.
1256+
To set a key, run \'swift post -m
1257+
"Temp-URL-Key:b3968d0207b54ece87cccc06515a89d4"\'
1258+
'''.strip('\n')
1259+
1260+
1261+
def st_tempurl(parser, args, thread_manager):
1262+
(options, args) = parse_args(parser, args)
1263+
args = args[1:]
1264+
if len(args) < 4:
1265+
thread_manager.error('Usage: %s tempurl %s\n%s', BASENAME,
1266+
st_tempurl_options, st_tempurl_help)
1267+
return
1268+
method, seconds, path, key = args[:4]
1269+
try:
1270+
seconds = int(seconds)
1271+
except ValueError:
1272+
thread_manager.error('Seconds must be an integer')
1273+
return
1274+
if method.upper() not in ['GET', 'PUT', 'HEAD', 'POST', 'DELETE']:
1275+
thread_manager.print_msg('WARNING: Non default HTTP method %s for '
1276+
'tempurl specified, possibly an error' %
1277+
method.upper())
1278+
url = generate_temp_url(path, seconds, key, method)
1279+
thread_manager.print_msg(url)
1280+
1281+
12431282
def split_headers(options, prefix='', thread_manager=None):
12441283
"""
12451284
Splits 'Key: Value' strings and returns them as a dictionary.
@@ -1269,6 +1308,10 @@ def parse_args(parser, args, enforce_requires=True):
12691308
args = ['-h']
12701309
(options, args) = parser.parse_args(args)
12711310

1311+
# Short circuit for tempurl, which doesn't need auth
1312+
if len(args) > 0 and args[0] == 'tempurl':
1313+
return options, args
1314+
12721315
if (not (options.auth and options.user and options.key)):
12731316
# Use 2.0 auth if none of the old args are present
12741317
options.auth_version = '2.0'
@@ -1351,6 +1394,7 @@ def main(arguments=None):
13511394
or object.
13521395
upload Uploads files or directories to the given container.
13531396
capabilities List cluster capabilities.
1397+
tempurl Create a temporary URL
13541398
13551399
13561400
Examples:
@@ -1488,7 +1532,7 @@ def main(arguments=None):
14881532
parser.enable_interspersed_args()
14891533

14901534
commands = ('delete', 'download', 'list', 'post',
1491-
'stat', 'upload', 'capabilities', 'info')
1535+
'stat', 'upload', 'capabilities', 'info', 'tempurl')
14921536
if not args or args[0] not in commands:
14931537
parser.print_usage()
14941538
if args:

swiftclient/utils.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
"""Miscellaneous utility functions for use with Swift."""
16+
import hashlib
17+
import hmac
18+
import logging
19+
import time
1620

1721
import six
1822

@@ -59,6 +63,51 @@ def prt_bytes(bytes, human_flag):
5963
return(bytes)
6064

6165

66+
def generate_temp_url(path, seconds, key, method):
67+
""" Generates a temporary URL that gives unauthenticated access to the
68+
Swift object.
69+
70+
:param path: The full path to the Swift object. Example:
71+
/v1/AUTH_account/c/o.
72+
:param seconds: The amount of time in seconds the temporary URL will
73+
be valid for.
74+
:param key: The secret temporary URL key set on the Swift cluster.
75+
To set a key, run 'swift post -m
76+
"Temp-URL-Key:b3968d0207b54ece87cccc06515a89d4"'
77+
:param method: A HTTP method, typically either GET or PUT, to allow for
78+
this temporary URL.
79+
:raises: ValueError if seconds is not a positive integer
80+
:raises: TypeError if seconds is not an integer
81+
:return: the path portion of a temporary URL
82+
"""
83+
if seconds < 0:
84+
raise ValueError('seconds must be a positive integer')
85+
try:
86+
expiration = int(time.time() + seconds)
87+
except TypeError:
88+
raise TypeError('seconds must be an integer')
89+
90+
standard_methods = ['GET', 'PUT', 'HEAD', 'POST', 'DELETE']
91+
if method.upper() not in standard_methods:
92+
logger = logging.getLogger("swiftclient")
93+
logger.warning('Non default HTTP method %s for tempurl specified, '
94+
'possibly an error', method.upper())
95+
96+
hmac_body = '\n'.join([method.upper(), str(expiration), path])
97+
98+
# Encode to UTF-8 for py3 compatibility
99+
sig = hmac.new(key.encode(),
100+
hmac_body.encode(),
101+
hashlib.sha1).hexdigest()
102+
103+
return ('{path}?temp_url_sig='
104+
'{sig}&temp_url_expires={exp}'.format(
105+
path=path,
106+
sig=sig,
107+
exp=expiration)
108+
)
109+
110+
62111
class LengthWrapper(object):
63112

64113
def __init__(self, readable, length):

tests/unit/test_shell.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import swiftclient
2424
import swiftclient.shell
25+
import swiftclient.utils
2526

2627

2728
if six.PY2:
@@ -328,6 +329,16 @@ def test_post_object(self, connection):
328329
'Content-Type': 'text/plain',
329330
'X-Object-Meta-Color': 'Blue'})
330331

332+
@mock.patch('swiftclient.shell.generate_temp_url')
333+
def test_temp_url(self, temp_url):
334+
argv = ["", "tempurl", "GET", "60", "/v1/AUTH_account/c/o",
335+
"secret_key"
336+
]
337+
temp_url.return_value = ""
338+
swiftclient.shell.main(argv)
339+
temp_url.assert_called_with(
340+
'/v1/AUTH_account/c/o', 60, 'secret_key', 'GET')
341+
331342
@mock.patch('swiftclient.shell.Connection')
332343
def test_capabilities(self, connection):
333344
argv = ["", "capabilities"]

tests/unit/test_utils.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import testtools
1717

18+
import mock
1819
import six
1920
import tempfile
2021

@@ -122,6 +123,44 @@ def test_overflow(self):
122123
self.assertEqual('1024Y', u.prt_bytes(bytes_, True).lstrip())
123124

124125

126+
class TestTempURL(testtools.TestCase):
127+
128+
def setUp(self):
129+
super(TestTempURL, self).setUp()
130+
self.url = '/v1/AUTH_account/c/o'
131+
self.seconds = 3600
132+
self.key = 'correcthorsebatterystaple'
133+
self.method = 'GET'
134+
135+
@mock.patch('hmac.HMAC.hexdigest')
136+
@mock.patch('time.time')
137+
def test_generate_temp_url(self, time_mock, hmac_mock):
138+
time_mock.return_value = 1400000000
139+
hmac_mock.return_value = 'temp_url_signature'
140+
expected_url = (
141+
'/v1/AUTH_account/c/o?'
142+
'temp_url_sig=temp_url_signature&'
143+
'temp_url_expires=1400003600')
144+
url = u.generate_temp_url(self.url, self.seconds, self.key,
145+
self.method)
146+
self.assertEqual(url, expected_url)
147+
148+
def test_generate_temp_url_bad_seconds(self):
149+
self.assertRaises(TypeError,
150+
u.generate_temp_url,
151+
self.url,
152+
'not_an_int',
153+
self.key,
154+
self.method)
155+
156+
self.assertRaises(ValueError,
157+
u.generate_temp_url,
158+
self.url,
159+
-1,
160+
self.key,
161+
self.method)
162+
163+
125164
class TestLengthWrapper(testtools.TestCase):
126165

127166
def test_stringio(self):

0 commit comments

Comments
 (0)