diff --git a/swift/common/middleware/tempurl.py b/swift/common/middleware/tempurl.py index c90c8d0b40..c86f32e01a 100644 --- a/swift/common/middleware/tempurl.py +++ b/swift/common/middleware/tempurl.py @@ -66,9 +66,15 @@ Using this in combination with browser form post translation middleware could also allow direct-from-browser uploads to specific locations in Swift. -Note that changing the X-Account-Meta-Temp-URL-Key will invalidate -any previously generated temporary URLs within 60 seconds (the -memcache time for the key). +TempURL supports up to two keys, specified by X-Account-Meta-Temp-URL-Key and +X-Account-Meta-Temp-URL-Key-2. Signatures are checked against both keys, if +present. This is to allow for key rotation without invalidating all existing +temporary URLs. + +Note that changing either X-Account-Meta-Temp-URL-Key or +X-Account-Meta-Temp-URL-Key-2 will invalidate any previously generated +temporary URLs signed with that key within 60 seconds (the memcache lifetime +for the key). It is not instantaneous. With GET TempURLs, a Content-Disposition header will be set on the response so that browsers will interpret this as a file attachment to @@ -246,20 +252,20 @@ class TempURL(object): account = self._get_account(env) if not account: return self._invalid(env, start_response) - key = self._get_key(env, account) - if not key: + keys = self._get_keys(env, account) + if not keys: return self._invalid(env, start_response) if env['REQUEST_METHOD'] == 'HEAD': - hmac_val = self._get_hmac(env, temp_url_expires, key, - request_method='GET') - if temp_url_sig != hmac_val: - hmac_val = self._get_hmac(env, temp_url_expires, key, - request_method='PUT') - if temp_url_sig != hmac_val: + hmac_vals = self._get_hmacs(env, temp_url_expires, keys, + request_method='GET') + if temp_url_sig not in hmac_vals: + hmac_vals = self._get_hmacs(env, temp_url_expires, keys, + request_method='PUT') + if temp_url_sig not in hmac_vals: return self._invalid(env, start_response) else: - hmac_val = self._get_hmac(env, temp_url_expires, key) - if temp_url_sig != hmac_val: + hmac_vals = self._get_hmacs(env, temp_url_expires, keys) + if temp_url_sig not in hmac_vals: return self._invalid(env, start_response) self._clean_incoming_headers(env) env['swift.authorize'] = lambda req: None @@ -339,40 +345,57 @@ class TempURL(object): filename = qs['filename'][0] return temp_url_sig, temp_url_expires, filename - def _get_key(self, env, account): + def _get_keys(self, env, account): """ - Returns the X-Account-Meta-Temp-URL-Key header value for the - account, or None if none is set. + Returns the X-Account-Meta-Temp-URL-Key[-2] header values for the + account, or an empty list if none is set. + + Returns 0, 1, or 2 elements depending on how many keys are set + in the account's metadata. :param env: The WSGI environment for the request. :param account: Account str. - :returns: X-Account-Meta-Temp-URL-Key str value, or None. + :returns: [X-Account-Meta-Temp-URL-Key str value if set, + X-Account-Meta-Temp-URL-Key-2 str value if set] """ - key = None + keys = None memcache = env.get('swift.cache') + memcache_hash_key = 'temp-url-keys/%s' % account if memcache: - key = memcache.get('temp-url-key/%s' % account) - if not key: + keys = memcache.get(memcache_hash_key) + if keys is None: newenv = make_pre_authed_env(env, 'HEAD', '/v1/' + account, self.agent, swift_source='TU') newenv['CONTENT_LENGTH'] = '0' newenv['wsgi.input'] = StringIO('') - key = [None] + keys = [] def _start_response(status, response_headers, exc_info=None): for h, v in response_headers: if h.lower() == 'x-account-meta-temp-url-key': - key[0] = v + keys.append(v) + elif h.lower() == 'x-account-meta-temp-url-key-2': + keys.append(v) i = iter(self.app(newenv, _start_response)) try: i.next() except StopIteration: pass - key = key[0] - if key and memcache: - memcache.set('temp-url-key/%s' % account, key, time=60) - return key + if memcache: + memcache.set(memcache_hash_key, keys, time=60) + return keys + + def _get_hmacs(self, env, expires, keys, request_method=None): + """ + :param env: The WSGI environment for the request. + :param expires: Unix timestamp as an int for when the URL + expires. + :param keys: Key strings, from the X-Account-Meta-Temp-URL-Key[-2] of + the account. + """ + return [self._get_hmac(env, expires, key, request_method) + for key in keys] def _get_hmac(self, env, expires, key, request_method=None): """ diff --git a/test/unit/common/middleware/test_tempurl.py b/test/unit/common/middleware/test_tempurl.py index a7066912aa..fd58f506c4 100644 --- a/test/unit/common/middleware/test_tempurl.py +++ b/test/unit/common/middleware/test_tempurl.py @@ -110,7 +110,7 @@ class TestTempURL(unittest.TestCase): req = self._make_request(path, environ={'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) self.tempurl.app = FakeApp(iter([('200 Ok', (), '123')])) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 200) @@ -119,6 +119,28 @@ class TestTempURL(unittest.TestCase): self.assertEquals(req.environ['swift.authorize_override'], True) self.assertEquals(req.environ['REMOTE_USER'], '.wsgi.tempurl') + def test_get_valid_key2(self): + method = 'GET' + expires = int(time() + 86400) + path = '/v1/a/c/o' + key1 = 'abc123' + key2 = 'def456' + hmac_body = '%s\n%s\n%s' % (method, expires, path) + sig1 = hmac.new(key1, hmac_body, sha1).hexdigest() + sig2 = hmac.new(key2, hmac_body, sha1).hexdigest() + for sig in (sig1, sig2): + req = self._make_request(path, + environ={'QUERY_STRING': + 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) + req.environ['swift.cache'].set('temp-url-keys/a', [key1, key2]) + self.tempurl.app = FakeApp(iter([('200 Ok', (), '123')])) + resp = req.get_response(self.tempurl) + self.assertEquals(resp.status_int, 200) + self.assertEquals(resp.headers['content-disposition'], + 'attachment; filename="o"') + self.assertEquals(req.environ['swift.authorize_override'], True) + self.assertEquals(req.environ['REMOTE_USER'], '.wsgi.tempurl') + def test_get_valid_with_filename(self): method = 'GET' expires = int(time() + 86400) @@ -129,7 +151,7 @@ class TestTempURL(unittest.TestCase): req = self._make_request(path, environ={ 'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s&' 'filename=bob%%20%%22killer%%22.txt' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) self.tempurl.app = FakeApp(iter([('200 Ok', (), '123')])) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 200) @@ -148,7 +170,7 @@ class TestTempURL(unittest.TestCase): req = self._make_request(path, environ={'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 404) self.assertFalse('content-disposition' in resp.headers) @@ -166,7 +188,7 @@ class TestTempURL(unittest.TestCase): environ={'REQUEST_METHOD': 'PUT', 'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 401) self.assertTrue('Temp URL invalid' in resp.body) @@ -182,7 +204,7 @@ class TestTempURL(unittest.TestCase): environ={'REQUEST_METHOD': 'PUT', 'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 404) self.assertEquals(req.environ['swift.authorize_override'], True) @@ -198,7 +220,7 @@ class TestTempURL(unittest.TestCase): req = self._make_request(path, environ={'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 401) self.assertTrue('Temp URL invalid' in resp.body) @@ -212,7 +234,7 @@ class TestTempURL(unittest.TestCase): hmac.new(key, hmac_body, sha1).hexdigest() req = self._make_request(path, environ={'QUERY_STRING': 'temp_url_expires=%s' % expires}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 401) self.assertTrue('Temp URL invalid' in resp.body) @@ -271,7 +293,7 @@ class TestTempURL(unittest.TestCase): environ={'REQUEST_METHOD': 'HEAD', 'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 404) self.assertEquals(req.environ['swift.authorize_override'], True) @@ -288,7 +310,7 @@ class TestTempURL(unittest.TestCase): environ={'REQUEST_METHOD': 'HEAD', 'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 404) self.assertEquals(req.environ['swift.authorize_override'], True) @@ -356,7 +378,7 @@ class TestTempURL(unittest.TestCase): environ={'REQUEST_METHOD': 'DELETE', 'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 404) @@ -371,7 +393,7 @@ class TestTempURL(unittest.TestCase): environ={'REQUEST_METHOD': 'UNKNOWN', 'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 401) self.assertTrue('Temp URL invalid' in resp.body) @@ -386,7 +408,7 @@ class TestTempURL(unittest.TestCase): req = self._make_request(path + '2', environ={'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 401) self.assertTrue('Temp URL invalid' in resp.body) @@ -405,7 +427,7 @@ class TestTempURL(unittest.TestCase): req = self._make_request(path, environ={'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 401) self.assertTrue('Temp URL invalid' in resp.body) @@ -421,7 +443,7 @@ class TestTempURL(unittest.TestCase): environ={'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires + 1)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 401) self.assertTrue('Temp URL invalid' in resp.body) @@ -436,7 +458,7 @@ class TestTempURL(unittest.TestCase): req = self._make_request(path, environ={'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key + '2') + req.environ['swift.cache'].set('temp-url-keys/a', [key + '2']) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 401) self.assertTrue('Temp URL invalid' in resp.body) @@ -453,7 +475,7 @@ class TestTempURL(unittest.TestCase): req = self._make_request(path, headers={'x-remove-this': 'value'}, environ={'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 404) self.assertTrue('x-remove-this' not in self.app.request.headers) @@ -473,7 +495,7 @@ class TestTempURL(unittest.TestCase): 'x-remove-this-except-this': 'value2'}, environ={'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 404) self.assertTrue('x-remove-this-one' not in self.app.request.headers) @@ -492,7 +514,7 @@ class TestTempURL(unittest.TestCase): req = self._make_request(path, environ={'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 404) self.assertTrue('x-test-header-one-a' not in resp.headers) @@ -511,7 +533,7 @@ class TestTempURL(unittest.TestCase): req = self._make_request(path, environ={'QUERY_STRING': 'temp_url_sig=%s&temp_url_expires=%s' % (sig, expires)}) - req.environ['swift.cache'].set('temp-url-key/a', key) + req.environ['swift.cache'].set('temp-url-keys/a', [key]) resp = req.get_response(self.tempurl) self.assertEquals(resp.status_int, 404) self.assertEquals(resp.headers['x-test-header-one-a'], 'value1') @@ -571,25 +593,44 @@ class TestTempURL(unittest.TestCase): def test_get_key_memcache(self): self.app.status_headers_body_iter = iter([('404 Not Found', {}, '')]) self.assertEquals( - self.tempurl._get_key({}, 'a'), None) + self.tempurl._get_keys({}, 'a'), []) self.app.status_headers_body_iter = iter([('404 Not Found', {}, '')]) self.assertEquals( - self.tempurl._get_key({'swift.cache': None}, 'a'), None) + self.tempurl._get_keys({'swift.cache': None}, 'a'), []) mc = FakeMemcache() self.app.status_headers_body_iter = iter([('404 Not Found', {}, '')]) self.assertEquals( - self.tempurl._get_key({'swift.cache': mc}, 'a'), None) - mc.set('temp-url-key/a', 'abc') + self.tempurl._get_keys({'swift.cache': mc}, 'a'), []) + mc.set('temp-url-keys/a', ['abc', 'def']) self.assertEquals( - self.tempurl._get_key({'swift.cache': mc}, 'a'), 'abc') + self.tempurl._get_keys({'swift.cache': mc}, 'a'), ['abc', 'def']) - def test_get_key_from_source(self): + def test_get_keys_from_source(self): self.app.status_headers_body_iter = \ iter([('200 Ok', {'x-account-meta-temp-url-key': 'abc'}, '')]) mc = FakeMemcache() self.assertEquals( - self.tempurl._get_key({'swift.cache': mc}, 'a'), 'abc') - self.assertEquals(mc.get('temp-url-key/a'), 'abc') + self.tempurl._get_keys({'swift.cache': mc}, 'a'), ['abc']) + self.assertEquals(mc.get('temp-url-keys/a'), ['abc']) + + self.app.status_headers_body_iter = \ + iter([('200 Ok', + {'x-account-meta-temp-url-key': 'abc', + 'x-account-meta-temp-url-key-2': 'def'}, + '')]) + mc = FakeMemcache() + self.assertEquals( + sorted(self.tempurl._get_keys({'swift.cache': mc}, 'a')), + ['abc', 'def']) + self.assertEquals(sorted(mc.get('temp-url-keys/a')), ['abc', 'def']) + + # no keys at all: still gets cached + self.app.status_headers_body_iter = iter([('200 Ok', {}, '')]) + mc = FakeMemcache() + self.assertEquals( + sorted(self.tempurl._get_keys({'swift.cache': mc}, 'a')), + []) + self.assertEquals(sorted(mc.get('temp-url-keys/a')), []) def test_get_hmac(self): self.assertEquals(self.tempurl._get_hmac(