-
-
Save clayg/11e4e9c0c3f652d82e0f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/swift/common/middleware/tempurl.py b/swift/common/middleware/tempurl.py | |
index 3dd1448..6767563 100644 | |
--- a/swift/common/middleware/tempurl.py | |
+++ b/swift/common/middleware/tempurl.py | |
@@ -150,6 +150,10 @@ DEFAULT_OUTGOING_REMOVE_HEADERS = 'x-object-meta-*' | |
DEFAULT_OUTGOING_ALLOW_HEADERS = 'x-object-meta-public-*' | |
+CONTAINER_SCOPE = 'container' | |
+ACCOUNT_SCOPE = 'account' | |
+ | |
+ | |
def get_tempurl_keys_from_metadata(meta): | |
""" | |
Extracts the tempurl keys from metadata. | |
@@ -170,6 +174,38 @@ def disposition_format(filename): | |
quote(filename, safe=' /'), quote(filename)) | |
+def authorize_same_account(account_to_match): | |
+ | |
+ def auth_callback_same_account(req): | |
+ try: | |
+ _ver, acc, _rest = req.split_path(2, 3, True) | |
+ except ValueError: | |
+ return None | |
+ | |
+ if acc == account_to_match: | |
+ return None | |
+ else: | |
+ return HTTPUnauthorized(request=req) | |
+ | |
+ return auth_callback_same_account | |
+ | |
+ | |
+def authorize_same_container(account_to_match, container_to_match): | |
+ | |
+ def auth_callback_same_container(req): | |
+ try: | |
+ _ver, acc, con, _rest = req.split_path(3, 4, True) | |
+ except ValueError: | |
+ return None | |
+ | |
+ if acc == account_to_match and con == container_to_match: | |
+ return None | |
+ else: | |
+ return HTTPUnauthorized(request=req) | |
+ | |
+ return auth_callback_same_container | |
+ | |
+ | |
class TempURL(object): | |
""" | |
WSGI Middleware to grant temporary URLs specific access to Swift | |
@@ -298,10 +334,10 @@ class TempURL(object): | |
return self.app(env, start_response) | |
if not temp_url_sig or not temp_url_expires: | |
return self._invalid(env, start_response) | |
- account = self._get_account(env) | |
+ account, container = self._get_account_and_container(env) | |
if not account: | |
return self._invalid(env, start_response) | |
- keys = self._get_keys(env, account) | |
+ keys = self._get_keys(env) | |
if not keys: | |
return self._invalid(env, start_response) | |
if env['REQUEST_METHOD'] == 'HEAD': | |
@@ -316,15 +352,25 @@ class TempURL(object): | |
else: | |
hmac_vals = self._get_hmacs(env, temp_url_expires, keys) | |
- # While it's true that any() will short-circuit, this doesn't affect | |
- # the timing-attack resistance since the only way this will | |
- # short-circuit is when a valid signature is passed in. | |
- is_valid_hmac = any(streq_const_time(temp_url_sig, hmac) | |
- for hmac in hmac_vals) | |
+ is_valid_hmac = False | |
+ hmac_scope = None | |
+ for hmac, scope in hmac_vals: | |
+ # While it's true that we short-circuit, this doesn't affect the | |
+ # timing-attack resistance since the only way this will | |
+ # short-circuit is when a valid signature is passed in. | |
+ if streq_const_time(temp_url_sig, hmac): | |
+ is_valid_hmac = True | |
+ hmac_scope = scope | |
+ break | |
if not is_valid_hmac: | |
return self._invalid(env, start_response) | |
self._clean_incoming_headers(env) | |
- env['swift.authorize'] = lambda req: None | |
+ | |
+ if hmac_scope == ACCOUNT_SCOPE: | |
+ env['swift.authorize'] = authorize_same_account(account) | |
+ else: | |
+ env['swift.authorize'] = authorize_same_container(account, | |
+ container) | |
env['swift.authorize_override'] = True | |
env['REMOTE_USER'] = '.wsgi.tempurl' | |
qs = {'temp_url_sig': temp_url_sig, | |
@@ -365,22 +411,23 @@ class TempURL(object): | |
return self.app(env, _start_response) | |
- def _get_account(self, env): | |
+ def _get_account_and_container(self, env): | |
""" | |
- Returns just the account for the request, if it's an object | |
- request and one of the configured methods; otherwise, None is | |
+ Returns just the account and container for the request, if it's an | |
+ object request and one of the configured methods; otherwise, None is | |
returned. | |
:param env: The WSGI environment for the request. | |
- :returns: Account str or None. | |
+ :returns: (Account str, container str) or (None, None). | |
""" | |
if env['REQUEST_METHOD'] in self.methods: | |
try: | |
ver, acc, cont, obj = split_path(env['PATH_INFO'], 4, 4, True) | |
except ValueError: | |
- return None | |
+ return (None, None) | |
if ver == 'v1' and obj.strip('/'): | |
- return acc | |
+ return (acc, cont) | |
+ return (None, None) | |
def _get_temp_url_info(self, env): | |
""" | |
@@ -410,18 +457,23 @@ class TempURL(object): | |
inline = True | |
return temp_url_sig, temp_url_expires, filename, inline | |
- def _get_keys(self, env, account): | |
+ def _get_keys(self, env): | |
""" | |
Returns the X-[Account|Container]-Meta-Temp-URL-Key[-2] header values | |
- for the account or container, or an empty list if none are set. | |
+ for the account or container, or an empty list if none are set. Each | |
+ value comes as a 2-tuple (key, scope), where scope is either | |
+ CONTAINER_SCOPE or ACCOUNT_SCOPE. | |
Returns 0-4 elements depending on how many keys are set in the | |
account's or container's metadata. | |
:param env: The WSGI environment for the request. | |
- :param account: Account str. | |
- :returns: [X-Account-Meta-Temp-URL-Key str value if set, | |
- X-Account-Meta-Temp-URL-Key-2 str value if set] | |
+ :returns: [ | |
+ (X-Account-Meta-Temp-URL-Key str value, ACCOUNT_SCOPE) if set, | |
+ (X-Account-Meta-Temp-URL-Key-2 str value, ACCOUNT_SCOPE if set, | |
+ (X-Container-Meta-Temp-URL-Key str value, CONTAINER_SCOPE) if set, | |
+ (X-Container-Meta-Temp-URL-Key-2 str value, CONTAINER_SCOPE if set, | |
+ ] | |
""" | |
account_info = get_account_info(env, self.app, swift_source='TU') | |
account_keys = get_tempurl_keys_from_metadata(account_info['meta']) | |
@@ -430,25 +482,28 @@ class TempURL(object): | |
container_keys = get_tempurl_keys_from_metadata( | |
container_info.get('meta', [])) | |
- return account_keys + container_keys | |
+ return ([(ak, ACCOUNT_SCOPE) for ak in account_keys] + | |
+ [(ck, CONTAINER_SCOPE) for ck in container_keys]) | |
- def _get_hmacs(self, env, expires, keys, request_method=None): | |
+ def _get_hmacs(self, env, expires, scoped_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. | |
+ :param scoped_keys: (key, scope) tuples like _get_keys() returns | |
:param request_method: Optional override of the request in | |
the WSGI env. For example, if a HEAD | |
does not match, you may wish to | |
override with GET to still allow the | |
HEAD. | |
+ | |
+ :returns: a list of (hmac, scope) 2-tuples | |
""" | |
if not request_method: | |
request_method = env['REQUEST_METHOD'] | |
- return [get_hmac( | |
- request_method, env['PATH_INFO'], expires, key) for key in keys] | |
+ return [ | |
+ (get_hmac(request_method, env['PATH_INFO'], expires, key), scope) | |
+ for (key, scope) in scoped_keys] | |
def _invalid(self, env, start_response): | |
""" | |
diff --git a/swift/proxy/server.py b/swift/proxy/server.py | |
index b631542..6ddc082 100644 | |
--- a/swift/proxy/server.py | |
+++ b/swift/proxy/server.py | |
@@ -385,10 +385,19 @@ class Application(object): | |
# controller's method indicates it'd like to gather more | |
# information and try again later. | |
resp = req.environ['swift.authorize'](req) | |
- if not resp and not req.headers.get('X-Copy-From-Account') \ | |
- and not req.headers.get('Destination-Account'): | |
+ if not resp: | |
# No resp means authorized, no delayed recheck required. | |
- del req.environ['swift.authorize'] | |
+ print req.headers.get('X-Copy-From-Account') | |
+ print req.headers.get('Destination-Account') | |
+ print req.environ.get('swift.authorize_override') | |
+ # However, on cross-account-copy or an authorize_override | |
+ # request we'll leave the authorize hook in place so they | |
+ # can also validate subrequests | |
+ if not (req.headers.get('X-Copy-From-Account') or | |
+ req.headers.get('Destination-Account') or | |
+ req.environ.get('swift.authorize_override', | |
+ False)): | |
+ del req.environ['swift.authorize'] | |
else: | |
# Response indicates denial, but we might delay the denial | |
# and recheck later. If not delayed, return the error now. | |
diff --git a/test/unit/common/middleware/test_tempurl.py b/test/unit/common/middleware/test_tempurl.py | |
index e665563..5c80591 100644 | |
--- a/test/unit/common/middleware/test_tempurl.py | |
+++ b/test/unit/common/middleware/test_tempurl.py | |
@@ -29,6 +29,7 @@ | |
# limitations under the License. | |
import hmac | |
+import itertools | |
import unittest | |
from hashlib import sha1 | |
from time import time | |
@@ -44,10 +45,13 @@ class FakeApp(object): | |
self.calls = 0 | |
self.status_headers_body_iter = status_headers_body_iter | |
if not self.status_headers_body_iter: | |
- self.status_headers_body_iter = iter([('404 Not Found', { | |
- 'x-test-header-one-a': 'value1', | |
- 'x-test-header-two-a': 'value2', | |
- 'x-test-header-two-b': 'value3'}, '')]) | |
+ self.status_headers_body_iter = iter( | |
+ itertools.repeat(( | |
+ '404 Not Found', { | |
+ 'x-test-header-one-a': 'value1', | |
+ 'x-test-header-two-a': 'value2', | |
+ 'x-test-header-two-b': 'value3'}, | |
+ ''))) | |
self.request = None | |
def __call__(self, env, start_response): | |
@@ -581,6 +585,67 @@ class TestTempURL(unittest.TestCase): | |
self.assertTrue('Temp URL invalid' in resp.body) | |
self.assertTrue('Www-Authenticate' in resp.headers) | |
+ def test_authorize_limits_scope(self): | |
+ environ = { | |
+ 'swift.infocache': { | |
+ 'swift.container/a/c': { | |
+ 'meta': {'Temp-URL-Key': 'container-key'}}, | |
+ 'swift.container/a': { | |
+ 'meta': {'Temp-URL-Key': 'account-key'}}}} | |
+ | |
+ req_other_object = Request.blank("/v1/a/c/o2") | |
+ req_other_container = Request.blank("/v1/a/c2/o2") | |
+ req_other_account = Request.blank("/v1/a2/c2/o2") | |
+ | |
+ # A request with the account key limits the pre-authed scope to the | |
+ # account level. | |
+ method = 'GET' | |
+ expires = int(time() + 86400) | |
+ path = '/v1/a/c/o' | |
+ key = 'account-key' | |
+ hmac_body = '%s\n%s\n%s' % (method, expires, path) | |
+ sig = hmac.new(key, hmac_body, sha1).hexdigest() | |
+ | |
+ environ['REQUEST_METHOD'] = 'GET' | |
+ environ['QUERY_STRING'] = 'temp_url_sig=%s&temp_url_expires=%s' % ( | |
+ sig, expires) | |
+ | |
+ req = self._make_request(path, keys=[key], environ=environ) | |
+ resp = req.get_response(self.tempurl) | |
+ self.assertEquals(resp.status_int, 404) # sanity check | |
+ | |
+ authorize = req.environ['swift.authorize'] | |
+ # Requests for other objects happen if, for example, you're | |
+ # downloading a large object or creating a large-object manifest. | |
+ oo_resp = authorize(req_other_object) | |
+ self.assertEqual(oo_resp, None) | |
+ oc_resp = authorize(req_other_container) | |
+ self.assertEqual(oc_resp, None) | |
+ oa_resp = authorize(req_other_account) | |
+ self.assertEqual(oa_resp.status_int, 401) | |
+ | |
+ # A request with the container key limits the pre-authed scope to | |
+ # the container level; a different container in the same account is | |
+ # out of scope and thus forbidden. | |
+ key = 'container-key' | |
+ hmac_body = '%s\n%s\n%s' % (method, expires, path) | |
+ sig = hmac.new(key, hmac_body, sha1).hexdigest() | |
+ | |
+ environ['QUERY_STRING'] = 'temp_url_sig=%s&temp_url_expires=%s' % ( | |
+ sig, expires) | |
+ | |
+ req = self._make_request(path, keys=[key], environ=environ) | |
+ resp = req.get_response(self.tempurl) | |
+ self.assertEquals(resp.status_int, 404) # sanity check | |
+ | |
+ authorize = req.environ['swift.authorize'] | |
+ oo_resp = authorize(req_other_object) | |
+ self.assertEqual(oo_resp, None) | |
+ oc_resp = authorize(req_other_container) | |
+ self.assertEqual(oc_resp, None) | |
+ oa_resp = authorize(req_other_account) | |
+ self.assertEqual(oa_resp.status_int, 401) | |
+ | |
def test_changed_path_invalid(self): | |
method = 'GET' | |
expires = int(time() + 86400) | |
@@ -809,35 +874,38 @@ class TestTempURL(unittest.TestCase): | |
self.assertTrue('x-conflict-header-test' in resp.headers) | |
self.assertEqual(resp.headers['x-conflict-header-test'], 'value') | |
- def test_get_account(self): | |
- self.assertEquals(self.tempurl._get_account({ | |
- 'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/v1/a/c/o'}), 'a') | |
- self.assertEquals(self.tempurl._get_account({ | |
- 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/o'}), 'a') | |
- self.assertEquals(self.tempurl._get_account({ | |
- 'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/v1/a/c/o'}), 'a') | |
- self.assertEquals(self.tempurl._get_account({ | |
- 'REQUEST_METHOD': 'POST', 'PATH_INFO': '/v1/a/c/o'}), 'a') | |
- self.assertEquals(self.tempurl._get_account({ | |
- 'REQUEST_METHOD': 'DELETE', 'PATH_INFO': '/v1/a/c/o'}), 'a') | |
- self.assertEquals(self.tempurl._get_account({ | |
- 'REQUEST_METHOD': 'UNKNOWN', 'PATH_INFO': '/v1/a/c/o'}), None) | |
- self.assertEquals(self.tempurl._get_account({ | |
- 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/'}), None) | |
- self.assertEquals(self.tempurl._get_account({ | |
- 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c//////'}), None) | |
- self.assertEquals(self.tempurl._get_account({ | |
- 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c///o///'}), 'a') | |
- self.assertEquals(self.tempurl._get_account({ | |
- 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c'}), None) | |
- self.assertEquals(self.tempurl._get_account({ | |
- 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a//o'}), None) | |
- self.assertEquals(self.tempurl._get_account({ | |
- 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1//c/o'}), None) | |
- self.assertEquals(self.tempurl._get_account({ | |
- 'REQUEST_METHOD': 'GET', 'PATH_INFO': '//a/c/o'}), None) | |
- self.assertEquals(self.tempurl._get_account({ | |
- 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v2/a/c/o'}), None) | |
+ def test_get_account_and_container(self): | |
+ self.assertEquals(self.tempurl._get_account_and_container({ | |
+ 'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/v1/a/c/o'}), ('a', 'c')) | |
+ self.assertEquals(self.tempurl._get_account_and_container({ | |
+ 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/o'}), ('a', 'c')) | |
+ self.assertEquals(self.tempurl._get_account_and_container({ | |
+ 'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/v1/a/c/o'}), ('a', 'c')) | |
+ self.assertEquals(self.tempurl._get_account_and_container({ | |
+ 'REQUEST_METHOD': 'POST', 'PATH_INFO': '/v1/a/c/o'}), ('a', 'c')) | |
+ self.assertEquals(self.tempurl._get_account_and_container({ | |
+ 'REQUEST_METHOD': 'DELETE', 'PATH_INFO': '/v1/a/c/o'}), ('a', 'c')) | |
+ self.assertEquals(self.tempurl._get_account_and_container({ | |
+ 'REQUEST_METHOD': 'UNKNOWN', 'PATH_INFO': '/v1/a/c/o'}), | |
+ (None, None)) | |
+ self.assertEquals(self.tempurl._get_account_and_container({ | |
+ 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/'}), (None, None)) | |
+ self.assertEquals(self.tempurl._get_account_and_container({ | |
+ 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c//////'}), | |
+ (None, None)) | |
+ self.assertEquals(self.tempurl._get_account_and_container({ | |
+ 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c///o///'}), | |
+ ('a', 'c')) | |
+ self.assertEquals(self.tempurl._get_account_and_container({ | |
+ 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c'}), (None, None)) | |
+ self.assertEquals(self.tempurl._get_account_and_container({ | |
+ 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a//o'}), (None, None)) | |
+ self.assertEquals(self.tempurl._get_account_and_container({ | |
+ 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1//c/o'}), (None, None)) | |
+ self.assertEquals(self.tempurl._get_account_and_container({ | |
+ 'REQUEST_METHOD': 'GET', 'PATH_INFO': '//a/c/o'}), (None, None)) | |
+ self.assertEquals(self.tempurl._get_account_and_container({ | |
+ 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v2/a/c/o'}), (None, None)) | |
def test_get_temp_url_info(self): | |
s = 'f5d5051bddf5df7e27c628818738334f' | |
@@ -889,13 +957,13 @@ class TestTempURL(unittest.TestCase): | |
self.assertEquals( | |
self.tempurl._get_hmacs( | |
{'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/a/c/o'}, | |
- 1, ['abc']), | |
- ['026d7f7cc25256450423c7ad03fc9f5ffc1dab6d']) | |
+ 1, [('abc', 'account')]), | |
+ [('026d7f7cc25256450423c7ad03fc9f5ffc1dab6d', 'account')]) | |
self.assertEquals( | |
self.tempurl._get_hmacs( | |
{'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/v1/a/c/o'}, | |
- 1, ['abc'], request_method='GET'), | |
- ['026d7f7cc25256450423c7ad03fc9f5ffc1dab6d']) | |
+ 1, [('abc', 'account')], request_method='GET'), | |
+ [('026d7f7cc25256450423c7ad03fc9f5ffc1dab6d', 'account')]) | |
def test_invalid(self): | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment