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
This commit is contained in:
parent
31383e57bd
commit
51727c531a
@ -1091,8 +1091,10 @@ swiftinfo_sig:
|
||||
temp_url_expires:
|
||||
description: |
|
||||
The date and time in `UNIX Epoch time stamp
|
||||
format <https://en.wikipedia.org/wiki/Unix_time>`_ when the
|
||||
signature for temporary URLs expires. For example, ``1440619048``
|
||||
format <https://en.wikipedia.org/wiki/Unix_time>`_ or
|
||||
`ISO 8601 UTC timestamp <https://en.wikipedia.org/wiki/ISO_8601>`_
|
||||
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
|
||||
<http://docs.openstack.org/developer/swift/api/temporary
|
||||
|
@ -51,8 +51,9 @@ The example shows these elements:
|
||||
the allowed HTTP method, expiration date, full path to the object, and the
|
||||
secret key for the temporary URL.
|
||||
|
||||
**temp\_url\_expires**: Required. An expiration date as a UNIX Epoch timestamp,
|
||||
which is an integer value. For example, ``1390852007`` represents
|
||||
**temp\_url\_expires**: Required. An expiration date as a UNIX Epoch timestamp
|
||||
or ISO 8601 UTC timestamp. For example, ``1390852007`` or
|
||||
``2014-01-27T19:46:47Z`` can be used to represent
|
||||
``Mon, 27 Jan 2014 19:46:47 GMT``.
|
||||
|
||||
For more information, see `Epoch & Unix Timestamp Conversion
|
||||
@ -72,7 +73,7 @@ by all object names for which the URL is valid.
|
||||
|
||||
https://swift-cluster.example.com/v1/my_account/container/my_prefix/object
|
||||
?temp_url_sig=da39a3ee5e6b4b0d3255bfef95601890afd80709
|
||||
&temp_url_expires=1323479485
|
||||
&temp_url_expires=2011-12-10T01:11:25Z
|
||||
&temp_url_prefix=my_prefix
|
||||
|
||||
.. _secret_keys:
|
||||
@ -126,7 +127,9 @@ signature includes these elements:
|
||||
|
||||
- Expiry time. In the example for the HMAC-SHA1 signature for temporary
|
||||
URLs below, the expiry time is set to ``86400`` seconds (or 1 day)
|
||||
into the future.
|
||||
into the future. Please be aware that you have to use a UNIX timestamp
|
||||
for generating the signature (in the API request it is also allowed to
|
||||
use an ISO 8601 UTC timestamp).
|
||||
|
||||
- The path. Starting with ``/v1/`` onwards and including a container
|
||||
name and object. The path for prefix-based signatures must start with
|
||||
@ -168,7 +171,6 @@ temporary URLs:
|
||||
hmac_body = '%s\n%s\n%s' % (method, expires, path)
|
||||
signature = hmac.new(key, hmac_body, sha1).hexdigest()
|
||||
|
||||
|
||||
Do not URL-encode the path when you generate the HMAC-SHA1 signature.
|
||||
However, when you make the actual HTTP request, you should properly
|
||||
URL-encode the URL.
|
||||
@ -179,6 +181,14 @@ in :ref:`secret_keys`.
|
||||
For more information, see `RFC 2104: HMAC: Keyed-Hashing for Message
|
||||
Authentication <http://www.ietf.org/rfc/rfc2104.txt>`__.
|
||||
|
||||
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
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user