Skip to content

Instantly share code, notes, and snippets.

@clayg
Last active August 29, 2015 14:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save clayg/11e4e9c0c3f652d82e0f to your computer and use it in GitHub Desktop.
Save clayg/11e4e9c0c3f652d82e0f to your computer and use it in GitHub Desktop.
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