From 51727c531a865e3b5b1a316baf66a69dd9855df0 Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Thu, 19 Jan 2017 16:07:09 +0100 Subject: [PATCH] ISO 8601 timestamps for tempurl With this commit, the tempurl middleware accepts (besides the traditional unix timestamps) also timestamps according to the format '%Y-%m-%dT%H:%M:%SZ' (one acceptable form of ISO 8601). The idea is to make the tempurls more user-friendly, and has been formulated here: Change-Id: I346a0241060a9559d178b30e60c957792bbeb9f0 Implements: blueprint human-readable-tempurl-timestamp --- api-ref/source/parameters.yaml | 6 +- doc/source/api/temporary_url_middleware.rst | 20 +++- swift/common/middleware/tempurl.py | 22 ++++- test/functional/test_tempurl.py | 74 ++++++++------ test/unit/common/middleware/test_tempurl.py | 101 +++++++++++--------- 5 files changed, 137 insertions(+), 86 deletions(-) diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 1bb1ac67c5..f4bad3ccf0 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -1091,8 +1091,10 @@ swiftinfo_sig: temp_url_expires: description: | The date and time in `UNIX Epoch time stamp - format `_ when the - signature for temporary URLs expires. For example, ``1440619048`` + format `_ or + `ISO 8601 UTC timestamp `_ + when the signature for temporary URLs expires. + For example, ``1440619048`` or ``2015-08-26T19:57:28Z`` is equivalent to ``Mon, Wed, 26 Aug 2015 19:57:28 GMT``. For more information about temporary URLs, see `Temporary URL middleware `__. +If you want to transform a UNIX timestamp into an ISO 8601 UTC timestamp, +you can use following code snippet: + +.. code:: + + import time + time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(timestamp)) + Using the ``swift`` tool to generate a Temporary URL ~~~~~~~~~~~~~~~~~~~~~ diff --git a/swift/common/middleware/tempurl.py b/swift/common/middleware/tempurl.py index cf140edcba..c0f66c9032 100644 --- a/swift/common/middleware/tempurl.py +++ b/swift/common/middleware/tempurl.py @@ -82,6 +82,15 @@ Let's say ``sig`` ends up equaling temp_url_sig=da39a3ee5e6b4b0d3255bfef95601890afd80709& temp_url_expires=1323479485 +You may also use ISO 8601 UTC timestamps with the format +``"%Y-%m-%dT%H:%M:%SZ"`` instead of UNIX timestamps in the URL +(but NOT in the code above for generating the signature!). +So, the latter URL could also be formulated as: + + https://swift-cluster.example.com/v1/AUTH_account/container/object? + temp_url_sig=da39a3ee5e6b4b0d3255bfef95601890afd80709& + temp_url_expires=2011-12-10T01:11:25Z + If a prefix-based signature with the prefix ``pre`` is desired, set path to:: path = 'prefix:/v1/AUTH_account/container/pre' @@ -198,9 +207,9 @@ __all__ = ['TempURL', 'filter_factory', 'DEFAULT_OUTGOING_REMOVE_HEADERS', 'DEFAULT_OUTGOING_ALLOW_HEADERS'] - +from calendar import timegm from os.path import basename -from time import time, strftime, gmtime +from time import time, strftime, strptime, gmtime from six.moves.urllib.parse import parse_qs from six.moves.urllib.parse import urlencode @@ -241,6 +250,8 @@ DEFAULT_OUTGOING_ALLOW_HEADERS = 'x-object-meta-public-*' CONTAINER_SCOPE = 'container' ACCOUNT_SCOPE = 'account' +EXPIRES_ISO8601_FORMAT = '%Y-%m-%dT%H:%M:%SZ' + def get_tempurl_keys_from_metadata(meta): """ @@ -533,7 +544,12 @@ class TempURL(object): try: temp_url_expires = int(qs['temp_url_expires'][0]) except ValueError: - temp_url_expires = 0 + try: + temp_url_expires = timegm(strptime( + qs['temp_url_expires'][0], + EXPIRES_ISO8601_FORMAT)) + except ValueError: + temp_url_expires = 0 if temp_url_expires < time(): temp_url_expires = 0 if 'temp_url_prefix' in qs: diff --git a/test/functional/test_tempurl.py b/test/functional/test_tempurl.py index 50c696efdf..83f1ad5a3b 100644 --- a/test/functional/test_tempurl.py +++ b/test/functional/test_tempurl.py @@ -17,12 +17,13 @@ import hmac import hashlib import json -import time from copy import deepcopy from six.moves import urllib from unittest2 import SkipTest +from time import time, strftime, gmtime import test.functional as tf +from swift.common.middleware import tempurl from test.functional import cluster_info from test.functional.tests import Utils, Base, Base2, BaseEnv from test.functional import requires_acls @@ -98,7 +99,9 @@ class TestTempurl(Base): "Expected tempurl_enabled to be True/False, got %r" % (self.env.tempurl_enabled,)) - self.expires = int(time.time()) + 86400 + self.expires = int(time()) + 86400 + self.expires_8601 = strftime( + tempurl.EXPIRES_ISO8601_FORMAT, gmtime(self.expires)) self.obj_tempurl_parms = self.tempurl_parms( 'GET', self.expires, self.env.conn.make_path(self.env.obj.path), self.env.tempurl_key) @@ -111,17 +114,20 @@ class TestTempurl(Base): return {'temp_url_sig': sig, 'temp_url_expires': str(expires)} def test_GET(self): - contents = self.env.obj.read( - parms=self.obj_tempurl_parms, - cfg={'no_auth_token': True}) - self.assertEqual(contents, "obj contents") + for e in (str(self.expires), self.expires_8601): + self.obj_tempurl_parms['temp_url_expires'] = e - # GET tempurls also allow HEAD requests - self.assertTrue(self.env.obj.info(parms=self.obj_tempurl_parms, - cfg={'no_auth_token': True})) + contents = self.env.obj.read( + parms=self.obj_tempurl_parms, + cfg={'no_auth_token': True}) + self.assertEqual(contents, "obj contents") + + # GET tempurls also allow HEAD requests + self.assertTrue(self.env.obj.info(parms=self.obj_tempurl_parms, + cfg={'no_auth_token': True})) def test_GET_with_key_2(self): - expires = int(time.time()) + 86400 + expires = int(time()) + 86400 parms = self.tempurl_parms( 'GET', expires, self.env.conn.make_path(self.env.obj.path), self.env.tempurl_key2) @@ -143,7 +149,7 @@ class TestTempurl(Base): hdrs={"X-Object-Manifest": "%s/get-dlo-inside-seg" % (self.env.container.name,)}) - expires = int(time.time()) + 86400 + expires = int(time()) + 86400 parms = self.tempurl_parms( 'GET', expires, self.env.conn.make_path(manifest.path), self.env.tempurl_key) @@ -168,7 +174,7 @@ class TestTempurl(Base): hdrs={"X-Object-Manifest": "%s/get-dlo-outside-seg" % (self.env.container.name,)}) - expires = int(time.time()) + 86400 + expires = int(time()) + 86400 parms = self.tempurl_parms( 'GET', expires, self.env.conn.make_path(manifest.path), self.env.tempurl_key) @@ -181,24 +187,29 @@ class TestTempurl(Base): def test_PUT(self): new_obj = self.env.container.file(Utils.create_name()) - expires = int(time.time()) + 86400 + expires = int(time()) + 86400 + expires_8601 = strftime( + tempurl.EXPIRES_ISO8601_FORMAT, gmtime(expires)) + put_parms = self.tempurl_parms( 'PUT', expires, self.env.conn.make_path(new_obj.path), self.env.tempurl_key) + for e in (str(expires), expires_8601): + put_parms['temp_url_expires'] = e - new_obj.write('new obj contents', - parms=put_parms, cfg={'no_auth_token': True}) - self.assertEqual(new_obj.read(), "new obj contents") + new_obj.write('new obj contents', + parms=put_parms, cfg={'no_auth_token': True}) + self.assertEqual(new_obj.read(), "new obj contents") - # PUT tempurls also allow HEAD requests - self.assertTrue(new_obj.info(parms=put_parms, - cfg={'no_auth_token': True})) + # PUT tempurls also allow HEAD requests + self.assertTrue(new_obj.info(parms=put_parms, + cfg={'no_auth_token': True})) def test_PUT_manifest_access(self): new_obj = self.env.container.file(Utils.create_name()) # give out a signature which allows a PUT to new_obj - expires = int(time.time()) + 86400 + expires = int(time()) + 86400 put_parms = self.tempurl_parms( 'PUT', expires, self.env.conn.make_path(new_obj.path), self.env.tempurl_key) @@ -230,7 +241,7 @@ class TestTempurl(Base): # try again using a tempurl POST to an already created object new_obj.write('', {}, parms=put_parms, cfg={'no_auth_token': True}) - expires = int(time.time()) + 86400 + expires = int(time()) + 86400 post_parms = self.tempurl_parms( 'POST', expires, self.env.conn.make_path(new_obj.path), self.env.tempurl_key) @@ -243,24 +254,25 @@ class TestTempurl(Base): self.fail('request did not error') def test_HEAD(self): - expires = int(time.time()) + 86400 + expires = int(time()) + 86400 head_parms = self.tempurl_parms( 'HEAD', expires, self.env.conn.make_path(self.env.obj.path), self.env.tempurl_key) self.assertTrue(self.env.obj.info(parms=head_parms, cfg={'no_auth_token': True})) + # HEAD tempurls don't allow PUT or GET requests, despite the fact that # PUT and GET tempurls both allow HEAD requests self.assertRaises(ResponseError, self.env.other_obj.read, cfg={'no_auth_token': True}, - parms=self.obj_tempurl_parms) + parms=head_parms) self.assert_status([401]) self.assertRaises(ResponseError, self.env.other_obj.write, 'new contents', cfg={'no_auth_token': True}, - parms=self.obj_tempurl_parms) + parms=head_parms) self.assert_status([401]) def test_different_object(self): @@ -428,7 +440,7 @@ class TestContainerTempurl(Base): "Expected tempurl_enabled to be True/False, got %r" % (self.env.tempurl_enabled,)) - expires = int(time.time()) + 86400 + expires = int(time()) + 86400 sig = self.tempurl_sig( 'GET', expires, self.env.conn.make_path(self.env.obj.path), self.env.tempurl_key) @@ -452,7 +464,7 @@ class TestContainerTempurl(Base): cfg={'no_auth_token': True})) def test_GET_with_key_2(self): - expires = int(time.time()) + 86400 + expires = int(time()) + 86400 sig = self.tempurl_sig( 'GET', expires, self.env.conn.make_path(self.env.obj.path), self.env.tempurl_key2) @@ -465,7 +477,7 @@ class TestContainerTempurl(Base): def test_PUT(self): new_obj = self.env.container.file(Utils.create_name()) - expires = int(time.time()) + 86400 + expires = int(time()) + 86400 sig = self.tempurl_sig( 'PUT', expires, self.env.conn.make_path(new_obj.path), self.env.tempurl_key) @@ -481,7 +493,7 @@ class TestContainerTempurl(Base): cfg={'no_auth_token': True})) def test_HEAD(self): - expires = int(time.time()) + 86400 + expires = int(time()) + 86400 sig = self.tempurl_sig( 'HEAD', expires, self.env.conn.make_path(self.env.obj.path), self.env.tempurl_key) @@ -588,7 +600,7 @@ class TestContainerTempurl(Base): hdrs={"X-Object-Manifest": "%s/get-dlo-inside-seg" % (self.env.container.name,)}) - expires = int(time.time()) + 86400 + expires = int(time()) + 86400 sig = self.tempurl_sig( 'GET', expires, self.env.conn.make_path(manifest.path), self.env.tempurl_key) @@ -614,7 +626,7 @@ class TestContainerTempurl(Base): hdrs={"X-Object-Manifest": "%s/get-dlo-outside-seg" % (container2.name,)}) - expires = int(time.time()) + 86400 + expires = int(time()) + 86400 sig = self.tempurl_sig( 'GET', expires, self.env.conn.make_path(manifest.path), self.env.tempurl_key) @@ -701,7 +713,7 @@ class TestSloTempurl(Base): hashlib.sha1).hexdigest() def test_GET(self): - expires = int(time.time()) + 86400 + expires = int(time()) + 86400 sig = self.tempurl_sig( 'GET', expires, self.env.conn.make_path(self.env.manifest.path), self.env.tempurl_key) diff --git a/test/unit/common/middleware/test_tempurl.py b/test/unit/common/middleware/test_tempurl.py index dffd1a8617..0b3b1d3c35 100644 --- a/test/unit/common/middleware/test_tempurl.py +++ b/test/unit/common/middleware/test_tempurl.py @@ -1076,54 +1076,65 @@ class TestTempURL(unittest.TestCase): def test_get_temp_url_info(self): s = 'f5d5051bddf5df7e27c628818738334f' - e = int(time() + 86400) + e_ts = int(time() + 86400) + e_8601 = strftime(tempurl.EXPIRES_ISO8601_FORMAT, gmtime(e_ts)) + for e in (e_ts, e_8601): + self.assertEqual( + self.tempurl._get_temp_url_info( + {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % ( + s, e)}), + (s, e_ts, None, None, None)) + self.assertEqual( + self.tempurl._get_temp_url_info( + {'QUERY_STRING': + 'temp_url_sig=%s&temp_url_expires=%s&temp_url_prefix=%s' + % (s, e, 'prefix')}), + (s, e_ts, 'prefix', None, None)) + self.assertEqual( + self.tempurl._get_temp_url_info( + {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&' + 'filename=bobisyouruncle' % (s, e)}), + (s, e_ts, None, 'bobisyouruncle', None)) + self.assertEqual( + self.tempurl._get_temp_url_info({}), + (None, None, None, None, None)) + self.assertEqual( + self.tempurl._get_temp_url_info( + {'QUERY_STRING': 'temp_url_expires=%s' % e}), + (None, e_ts, None, None, None)) + self.assertEqual( + self.tempurl._get_temp_url_info( + {'QUERY_STRING': 'temp_url_sig=%s' % s}), + (s, None, None, None, None)) + self.assertEqual( + self.tempurl._get_temp_url_info( + {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=bad' % ( + s)}), + (s, 0, None, None, None)) + self.assertEqual( + self.tempurl._get_temp_url_info( + {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&' + 'inline=' % (s, e)}), + (s, e_ts, None, None, True)) + self.assertEqual( + self.tempurl._get_temp_url_info( + {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&' + 'filename=bobisyouruncle&inline=' % (s, e)}), + (s, e_ts, None, 'bobisyouruncle', True)) + e_ts = int(time() - 1) + e_8601 = strftime(tempurl.EXPIRES_ISO8601_FORMAT, gmtime(e_ts)) + for e in (e_ts, e_8601): + self.assertEqual( + self.tempurl._get_temp_url_info( + {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % ( + s, e)}), + (s, 0, None, None, None)) + # Offsets not supported (yet?). + e_8601 = strftime('%Y-%m-%dT%H:%M:%S+0000', gmtime(e_ts)) self.assertEqual( self.tempurl._get_temp_url_info( {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % ( - s, e)}), - (s, e, None, None, None)) - self.assertEqual( - self.tempurl._get_temp_url_info( - {'QUERY_STRING': - 'temp_url_sig=%s&temp_url_expires=%s&temp_url_prefix=%s' % ( - s, e, 'prefix')}), - (s, e, 'prefix', None, None)) - self.assertEqual( - self.tempurl._get_temp_url_info( - {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&' - 'filename=bobisyouruncle' % (s, e)}), - (s, e, None, 'bobisyouruncle', None)) - self.assertEqual( - self.tempurl._get_temp_url_info({}), - (None, None, None, None, None)) - self.assertEqual( - self.tempurl._get_temp_url_info( - {'QUERY_STRING': 'temp_url_expires=%s' % e}), - (None, e, None, None, None)) - self.assertEqual( - self.tempurl._get_temp_url_info( - {'QUERY_STRING': 'temp_url_sig=%s' % s}), - (s, None, None, None, None)) - self.assertEqual( - self.tempurl._get_temp_url_info( - {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=bad' % ( - s)}), - (s, 0, None, None, None)) - self.assertEqual( - self.tempurl._get_temp_url_info( - {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&' - 'inline=' % (s, e)}), - (s, e, None, None, True)) - self.assertEqual( - self.tempurl._get_temp_url_info( - {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&' - 'filename=bobisyouruncle&inline=' % (s, e)}), - (s, e, None, 'bobisyouruncle', True)) - e = int(time() - 1) - self.assertEqual( - self.tempurl._get_temp_url_info( - {'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % ( - s, e)}), + s, e_8601)}), (s, 0, None, None, None)) def test_get_hmacs(self):