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, \
|
||||
config_true_value, drain_and_close
|
||||
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, \
|
||||
check_path_header, get_container_update_override_key, \
|
||||
update_ignore_range_header
|
||||
@ -442,7 +443,9 @@ class SymlinkObjectContext(WSGIContext):
|
||||
content_type='text/plain')
|
||||
|
||||
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)
|
||||
|
||||
def build_traversal_req(symlink_target):
|
||||
@ -457,9 +460,20 @@ class SymlinkObjectContext(WSGIContext):
|
||||
'/', version, account,
|
||||
symlink_target.lstrip('/'))
|
||||
self._last_target_path = target_path
|
||||
new_req = make_subrequest(
|
||||
req.environ, path=target_path, method=req.method,
|
||||
headers=req.headers, swift_source='SYM')
|
||||
|
||||
subreq_headers = dict(req.headers)
|
||||
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)
|
||||
return new_req
|
||||
|
||||
@ -484,11 +498,8 @@ class SymlinkObjectContext(WSGIContext):
|
||||
if not config_true_value(
|
||||
self._response_header_value(SYMLOOP_EXTEND)):
|
||||
self._loop_count += 1
|
||||
if config_true_value(
|
||||
self._response_header_value(ALLOW_RESERVED_NAMES)):
|
||||
new_req.headers['X-Backend-Allow-Reserved-Names'] = 'true'
|
||||
|
||||
return self._recursive_get_head(new_req, target_etag=resp_etag)
|
||||
return self._recursive_get_head(new_req, target_etag=resp_etag,
|
||||
orig_req=req)
|
||||
else:
|
||||
final_etag = self._response_header_value('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'})
|
||||
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):
|
||||
# sanity
|
||||
container = self.env.container
|
||||
@ -2712,16 +2758,11 @@ class TestVersioningContainerTempurl(TestObjectVersioningBase):
|
||||
obj.write(b"version2")
|
||||
|
||||
# get v2 object (reading from versions container)
|
||||
# cross container tempurl does not work for container tempurl key
|
||||
try:
|
||||
obj.read(parms=get_parms, cfg={'no_auth_token': True})
|
||||
except ResponseError as e:
|
||||
self.assertEqual(e.status, 401)
|
||||
else:
|
||||
self.fail('request did not error')
|
||||
try:
|
||||
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')
|
||||
# versioning symlink allows us to bypass the normal
|
||||
# container-tempurl-key scoping
|
||||
contents = obj.read(parms=get_parms, cfg={'no_auth_token': True})
|
||||
self.assert_status([200])
|
||||
self.assertEqual(contents, b"version2")
|
||||
# HEAD works, too
|
||||
obj.info(parms=get_parms, cfg={'no_auth_token': True})
|
||||
self.assert_status([200])
|
||||
|
@ -24,6 +24,7 @@ from swift.common import swob
|
||||
from swift.common.middleware import symlink, copy, versioned_writes, \
|
||||
listing_formats
|
||||
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 test.unit.common.middleware.helpers import FakeSwift
|
||||
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.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):
|
||||
self.app.register('GET', '/v1/a/c/symlink', swob.HTTPOk,
|
||||
{'X-Object-Sysmeta-Symlink-Target': 'c/sym1'})
|
||||
|
Loading…
Reference in New Issue
Block a user