versioning: Have versioning symlinks make pre-auth requests to reserved container
Previously, the lack of container ACLs on the reserved container would mean that attempting to grant access to the user-visible container would not work; the user could not access the backing object. Now, have symlinks with the allow-reserved-names sysmeta set be pre-authed. Note that the user still has to be authorized to read the symlink, and if the backing object was *itself* a symlink, that will be authed separately. Change-Id: Ifd744044421ef2ca917ce9502b155a6514ce8ecf Closes-Bug: #1880013
This commit is contained in:
parent
63e02fa9fa
commit
a8e03f42e0
|
@ -205,7 +205,8 @@ from swift.common.utils import get_logger, register_swift_info, split_path, \
|
||||||
MD5_OF_EMPTY_STRING, close_if_possible, closing_if_possible, \
|
MD5_OF_EMPTY_STRING, close_if_possible, closing_if_possible, \
|
||||||
config_true_value, drain_and_close
|
config_true_value, drain_and_close
|
||||||
from swift.common.constraints import check_account_format
|
from swift.common.constraints import check_account_format
|
||||||
from swift.common.wsgi import WSGIContext, make_subrequest
|
from swift.common.wsgi import WSGIContext, make_subrequest, \
|
||||||
|
make_pre_authed_request
|
||||||
from swift.common.request_helpers import get_sys_meta_prefix, \
|
from swift.common.request_helpers import get_sys_meta_prefix, \
|
||||||
check_path_header, get_container_update_override_key, \
|
check_path_header, get_container_update_override_key, \
|
||||||
update_ignore_range_header
|
update_ignore_range_header
|
||||||
|
@ -442,7 +443,9 @@ class SymlinkObjectContext(WSGIContext):
|
||||||
content_type='text/plain')
|
content_type='text/plain')
|
||||||
|
|
||||||
def _recursive_get_head(self, req, target_etag=None,
|
def _recursive_get_head(self, req, target_etag=None,
|
||||||
follow_softlinks=True):
|
follow_softlinks=True, orig_req=None):
|
||||||
|
if not orig_req:
|
||||||
|
orig_req = req
|
||||||
resp = self._app_call(req.environ)
|
resp = self._app_call(req.environ)
|
||||||
|
|
||||||
def build_traversal_req(symlink_target):
|
def build_traversal_req(symlink_target):
|
||||||
|
@ -457,9 +460,20 @@ class SymlinkObjectContext(WSGIContext):
|
||||||
'/', version, account,
|
'/', version, account,
|
||||||
symlink_target.lstrip('/'))
|
symlink_target.lstrip('/'))
|
||||||
self._last_target_path = target_path
|
self._last_target_path = target_path
|
||||||
new_req = make_subrequest(
|
|
||||||
req.environ, path=target_path, method=req.method,
|
subreq_headers = dict(req.headers)
|
||||||
headers=req.headers, swift_source='SYM')
|
if self._response_header_value(ALLOW_RESERVED_NAMES):
|
||||||
|
# this symlink's sysmeta says it can point to reserved names,
|
||||||
|
# we're infering that some piece of middleware had previously
|
||||||
|
# authorized this request because users can't access reserved
|
||||||
|
# names directly
|
||||||
|
subreq_meth = make_pre_authed_request
|
||||||
|
subreq_headers['X-Backend-Allow-Reserved-Names'] = 'true'
|
||||||
|
else:
|
||||||
|
subreq_meth = make_subrequest
|
||||||
|
new_req = subreq_meth(orig_req.environ, path=target_path,
|
||||||
|
method=req.method, headers=subreq_headers,
|
||||||
|
swift_source='SYM')
|
||||||
new_req.headers.pop('X-Backend-Storage-Policy-Index', None)
|
new_req.headers.pop('X-Backend-Storage-Policy-Index', None)
|
||||||
return new_req
|
return new_req
|
||||||
|
|
||||||
|
@ -484,11 +498,8 @@ class SymlinkObjectContext(WSGIContext):
|
||||||
if not config_true_value(
|
if not config_true_value(
|
||||||
self._response_header_value(SYMLOOP_EXTEND)):
|
self._response_header_value(SYMLOOP_EXTEND)):
|
||||||
self._loop_count += 1
|
self._loop_count += 1
|
||||||
if config_true_value(
|
return self._recursive_get_head(new_req, target_etag=resp_etag,
|
||||||
self._response_header_value(ALLOW_RESERVED_NAMES)):
|
orig_req=req)
|
||||||
new_req.headers['X-Backend-Allow-Reserved-Names'] = 'true'
|
|
||||||
|
|
||||||
return self._recursive_get_head(new_req, target_etag=resp_etag)
|
|
||||||
else:
|
else:
|
||||||
final_etag = self._response_header_value('etag')
|
final_etag = self._response_header_value('etag')
|
||||||
if final_etag and target_etag and target_etag != final_etag:
|
if final_etag and target_etag and target_etag != final_etag:
|
||||||
|
|
|
@ -355,6 +355,52 @@ class TestObjectVersioning(TestObjectVersioningBase):
|
||||||
v_obj.read(hdrs={'if-match': 'not-the-etag'})
|
v_obj.read(hdrs={'if-match': 'not-the-etag'})
|
||||||
self.assertEqual(412, cm.exception.status)
|
self.assertEqual(412, cm.exception.status)
|
||||||
|
|
||||||
|
def test_container_acls(self):
|
||||||
|
if tf.skip3:
|
||||||
|
raise SkipTest('Username3 not set')
|
||||||
|
|
||||||
|
obj = self.env.container.file(Utils.create_name())
|
||||||
|
resp = obj.write(b"data", return_resp=True)
|
||||||
|
version_id = resp.getheader('x-object-version-id')
|
||||||
|
self.assertIsNotNone(version_id)
|
||||||
|
|
||||||
|
with self.assertRaises(ResponseError) as cm:
|
||||||
|
obj.read(hdrs={'X-Auth-Token': self.env.conn3.storage_token})
|
||||||
|
self.assertEqual(403, cm.exception.status)
|
||||||
|
|
||||||
|
# Container ACLs work more or less like they always have
|
||||||
|
self.env.container.update_metadata(
|
||||||
|
hdrs={'X-Container-Read': self.env.conn3.user_acl})
|
||||||
|
self.assertEqual(b"data", obj.read(hdrs={
|
||||||
|
'X-Auth-Token': self.env.conn3.storage_token}))
|
||||||
|
|
||||||
|
# But the version-specifc GET still requires a swift owner
|
||||||
|
with self.assertRaises(ResponseError) as cm:
|
||||||
|
obj.read(hdrs={'X-Auth-Token': self.env.conn3.storage_token},
|
||||||
|
parms={'version-id': version_id})
|
||||||
|
self.assertEqual(403, cm.exception.status)
|
||||||
|
|
||||||
|
# If it's pointing to a symlink that points elsewhere, that still needs
|
||||||
|
# to be authed
|
||||||
|
tgt_name = Utils.create_name()
|
||||||
|
self.env.unversioned_container.file(tgt_name).write(b'link')
|
||||||
|
sym_tgt_header = quote(unquote('%s/%s' % (
|
||||||
|
self.env.unversioned_container.name, tgt_name)))
|
||||||
|
obj.write(hdrs={'X-Symlink-Target': sym_tgt_header})
|
||||||
|
|
||||||
|
# So, user1's good...
|
||||||
|
self.assertEqual(b'link', obj.read())
|
||||||
|
# ...but user3 can't
|
||||||
|
with self.assertRaises(ResponseError) as cm:
|
||||||
|
obj.read(hdrs={'X-Auth-Token': self.env.conn3.storage_token})
|
||||||
|
self.assertEqual(403, cm.exception.status)
|
||||||
|
|
||||||
|
# unless we add the acl to the unversioned_container
|
||||||
|
self.env.unversioned_container.update_metadata(
|
||||||
|
hdrs={'X-Container-Read': self.env.conn3.user_acl})
|
||||||
|
self.assertEqual(b'link', obj.read(
|
||||||
|
hdrs={'X-Auth-Token': self.env.conn3.storage_token}))
|
||||||
|
|
||||||
def _test_overwriting_setup(self, obj_name=None):
|
def _test_overwriting_setup(self, obj_name=None):
|
||||||
# sanity
|
# sanity
|
||||||
container = self.env.container
|
container = self.env.container
|
||||||
|
@ -2712,16 +2758,11 @@ class TestVersioningContainerTempurl(TestObjectVersioningBase):
|
||||||
obj.write(b"version2")
|
obj.write(b"version2")
|
||||||
|
|
||||||
# get v2 object (reading from versions container)
|
# get v2 object (reading from versions container)
|
||||||
# cross container tempurl does not work for container tempurl key
|
# versioning symlink allows us to bypass the normal
|
||||||
try:
|
# container-tempurl-key scoping
|
||||||
obj.read(parms=get_parms, cfg={'no_auth_token': True})
|
contents = obj.read(parms=get_parms, cfg={'no_auth_token': True})
|
||||||
except ResponseError as e:
|
self.assert_status([200])
|
||||||
self.assertEqual(e.status, 401)
|
self.assertEqual(contents, b"version2")
|
||||||
else:
|
# HEAD works, too
|
||||||
self.fail('request did not error')
|
obj.info(parms=get_parms, cfg={'no_auth_token': True})
|
||||||
try:
|
self.assert_status([200])
|
||||||
obj.info(parms=get_parms, cfg={'no_auth_token': True})
|
|
||||||
except ResponseError as e:
|
|
||||||
self.assertEqual(e.status, 401)
|
|
||||||
else:
|
|
||||||
self.fail('request did not error')
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ from swift.common import swob
|
||||||
from swift.common.middleware import symlink, copy, versioned_writes, \
|
from swift.common.middleware import symlink, copy, versioned_writes, \
|
||||||
listing_formats
|
listing_formats
|
||||||
from swift.common.swob import Request
|
from swift.common.swob import Request
|
||||||
|
from swift.common.request_helpers import get_reserved_name
|
||||||
from swift.common.utils import MD5_OF_EMPTY_STRING, get_swift_info
|
from swift.common.utils import MD5_OF_EMPTY_STRING, get_swift_info
|
||||||
from test.unit.common.middleware.helpers import FakeSwift
|
from test.unit.common.middleware.helpers import FakeSwift
|
||||||
from test.unit.common.middleware.test_versioned_writes import FakeCache
|
from test.unit.common.middleware.test_versioned_writes import FakeCache
|
||||||
|
@ -618,6 +619,55 @@ class TestSymlinkMiddleware(TestSymlinkMiddlewareBase):
|
||||||
self.assertEqual(req_headers, calls[1].headers)
|
self.assertEqual(req_headers, calls[1].headers)
|
||||||
self.assertFalse(calls[2:])
|
self.assertFalse(calls[2:])
|
||||||
|
|
||||||
|
def test_get_symlink_to_reserved_object(self):
|
||||||
|
cont = get_reserved_name('versioned')
|
||||||
|
obj = get_reserved_name('symlink', '9999998765.99999')
|
||||||
|
symlink_target = "%s/%s" % (cont, obj)
|
||||||
|
version_path = '/v1/a/%s' % symlink_target
|
||||||
|
self.app.register('GET', '/v1/a/versioned/symlink', swob.HTTPOk, {
|
||||||
|
symlink.TGT_OBJ_SYSMETA_SYMLINK_HDR: symlink_target,
|
||||||
|
symlink.ALLOW_RESERVED_NAMES: 'true',
|
||||||
|
'x-object-sysmeta-symlink-target-etag': MD5_OF_EMPTY_STRING,
|
||||||
|
'x-object-sysmeta-symlink-target-bytes': '0',
|
||||||
|
})
|
||||||
|
self.app.register('GET', version_path, swob.HTTPOk, {})
|
||||||
|
req = Request.blank('/v1/a/versioned/symlink', headers={
|
||||||
|
'Range': 'foo', 'If-Match': 'bar'})
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '200 OK')
|
||||||
|
self.assertIn(('Content-Location', version_path), headers)
|
||||||
|
self.assertEqual(len(self.authorized), 1)
|
||||||
|
self.assertNotIn('X-Backend-Allow-Reserved-Names',
|
||||||
|
self.app.calls_with_headers[0])
|
||||||
|
call_headers = self.app.calls_with_headers[1].headers
|
||||||
|
self.assertEqual('true', call_headers[
|
||||||
|
'X-Backend-Allow-Reserved-Names'])
|
||||||
|
self.assertEqual('foo', call_headers['Range'])
|
||||||
|
self.assertEqual('bar', call_headers['If-Match'])
|
||||||
|
|
||||||
|
def test_get_symlink_to_reserved_symlink(self):
|
||||||
|
cont = get_reserved_name('versioned')
|
||||||
|
obj = get_reserved_name('symlink', '9999998765.99999')
|
||||||
|
symlink_target = "%s/%s" % (cont, obj)
|
||||||
|
version_path = '/v1/a/%s' % symlink_target
|
||||||
|
self.app.register('GET', '/v1/a/versioned/symlink', swob.HTTPOk, {
|
||||||
|
symlink.TGT_OBJ_SYSMETA_SYMLINK_HDR: symlink_target,
|
||||||
|
symlink.ALLOW_RESERVED_NAMES: 'true',
|
||||||
|
'x-object-sysmeta-symlink-target-etag': MD5_OF_EMPTY_STRING,
|
||||||
|
'x-object-sysmeta-symlink-target-bytes': '0',
|
||||||
|
})
|
||||||
|
self.app.register('GET', version_path, swob.HTTPOk, {
|
||||||
|
symlink.TGT_OBJ_SYSMETA_SYMLINK_HDR: 'unversioned/obj',
|
||||||
|
'ETag': MD5_OF_EMPTY_STRING,
|
||||||
|
})
|
||||||
|
self.app.register('GET', '/v1/a/unversioned/obj', swob.HTTPOk, {
|
||||||
|
})
|
||||||
|
req = Request.blank('/v1/a/versioned/symlink')
|
||||||
|
status, headers, body = self.call_sym(req)
|
||||||
|
self.assertEqual(status, '200 OK')
|
||||||
|
self.assertIn(('Content-Location', '/v1/a/unversioned/obj'), headers)
|
||||||
|
self.assertEqual(len(self.authorized), 2)
|
||||||
|
|
||||||
def test_symlink_too_deep(self):
|
def test_symlink_too_deep(self):
|
||||||
self.app.register('GET', '/v1/a/c/symlink', swob.HTTPOk,
|
self.app.register('GET', '/v1/a/c/symlink', swob.HTTPOk,
|
||||||
{'X-Object-Sysmeta-Symlink-Target': 'c/sym1'})
|
{'X-Object-Sysmeta-Symlink-Target': 'c/sym1'})
|
||||||
|
|
Loading…
Reference in New Issue