diff --git a/swift/common/middleware/symlink.py b/swift/common/middleware/symlink.py index d2c6444386..bde163aa0a 100644 --- a/swift/common/middleware/symlink.py +++ b/swift/common/middleware/symlink.py @@ -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: diff --git a/test/functional/test_object_versioning.py b/test/functional/test_object_versioning.py index f07672cd8c..eccb1e2d06 100644 --- a/test/functional/test_object_versioning.py +++ b/test/functional/test_object_versioning.py @@ -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]) diff --git a/test/unit/common/middleware/test_symlink.py b/test/unit/common/middleware/test_symlink.py index 01875ba48b..a5e6cbab4b 100644 --- a/test/unit/common/middleware/test_symlink.py +++ b/test/unit/common/middleware/test_symlink.py @@ -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'})