From cd693e519e71dfc95a0de389293a2df2523a7d70 Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Mon, 9 Mar 2020 13:45:58 -0700 Subject: [PATCH] encryption: Expose decrypted metadata via CORS Normally, the proxy object controller would be adding these, but when encrypted, there won't be any headers in the x-object-meta-* namespace. Closes-Bug: #1868045 Change-Id: I8e708a60ee63f679056300fc9d68227e46d605e8 --- swift/common/middleware/crypto/decrypter.py | 28 +++++++++++++++---- test/functional/test_object.py | 8 +++++- .../middleware/crypto/test_decrypter.py | 14 +++++++++- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/swift/common/middleware/crypto/decrypter.py b/swift/common/middleware/crypto/decrypter.py index 34dfef43bc..2ca3b2ec7c 100644 --- a/swift/common/middleware/crypto/decrypter.py +++ b/swift/common/middleware/crypto/decrypter.py @@ -197,7 +197,7 @@ class DecrypterObjContext(BaseDecrypterContext): result.append((new_prefix + short_name, decrypted_value)) return result - def decrypt_resp_headers(self, put_keys, post_keys): + def decrypt_resp_headers(self, put_keys, post_keys, update_cors_exposed): """ Find encrypted headers and replace with the decrypted versions. @@ -236,11 +236,27 @@ class DecrypterObjContext(BaseDecrypterContext): # that map to the same x-object-meta- header names i.e. decrypted # headers win over unexpected, unencrypted headers. if post_keys: - mod_hdr_pairs.extend(self.decrypt_user_metadata(post_keys)) + decrypted_meta = self.decrypt_user_metadata(post_keys) + mod_hdr_pairs.extend(decrypted_meta) + else: + decrypted_meta = [] mod_hdr_names = {h.lower() for h, v in mod_hdr_pairs} - mod_hdr_pairs.extend([(h, v) for h, v in self._response_headers - if h.lower() not in mod_hdr_names]) + + found_aceh = False + for header, value in self._response_headers: + lheader = header.lower() + if lheader in mod_hdr_names: + continue + if lheader == 'access-control-expose-headers': + found_aceh = True + mod_hdr_pairs.append((header, value + ', ' + ', '.join( + meta.lower() for meta, _data in decrypted_meta))) + else: + mod_hdr_pairs.append((header, value)) + if update_cors_exposed and not found_aceh: + mod_hdr_pairs.append(('Access-Control-Expose-Headers', ', '.join( + meta.lower() for meta, _data in decrypted_meta))) return mod_hdr_pairs def multipart_response_iter(self, resp, boundary, body_key, crypto_meta): @@ -326,7 +342,9 @@ class DecrypterObjContext(BaseDecrypterContext): self._response_exc_info) return app_resp - mod_resp_headers = self.decrypt_resp_headers(put_keys, post_keys) + mod_resp_headers = self.decrypt_resp_headers( + put_keys, post_keys, + update_cors_exposed=bool(req.headers.get('origin'))) if put_crypto_meta and req.method == 'GET' and \ is_success(self._get_status_int()): diff --git a/test/functional/test_object.py b/test/functional/test_object.py index 768de19c05..f29a3ffcd6 100644 --- a/test/functional/test_object.py +++ b/test/functional/test_object.py @@ -1542,7 +1542,7 @@ class TestObject(unittest.TestCase): def put_obj(url, token, parsed, conn, obj): conn.request( 'PUT', '%s/%s/%s' % (parsed.path, self.container, obj), - 'test', {'X-Auth-Token': token}) + 'test', {'X-Auth-Token': token, 'X-Object-Meta-Color': 'red'}) return check_response(conn) def check_cors(url, token, parsed, conn, @@ -1576,6 +1576,8 @@ class TestObject(unittest.TestCase): headers = dict((k.lower(), v) for k, v in resp.getheaders()) self.assertEqual(headers.get('access-control-allow-origin'), '*') + # Just a pre-flight; this doesn't show up yet + self.assertNotIn('access-control-expose-headers', headers) resp = retry(check_cors, 'GET', 'cat', {'Origin': 'http://m.com'}) @@ -1583,6 +1585,8 @@ class TestObject(unittest.TestCase): headers = dict((k.lower(), v) for k, v in resp.getheaders()) self.assertEqual(headers.get('access-control-allow-origin'), '*') + self.assertIn('x-object-meta-color', headers.get( + 'access-control-expose-headers').split(', ')) resp = retry(check_cors, 'GET', 'cat', {'Origin': 'http://m.com', @@ -1591,6 +1595,8 @@ class TestObject(unittest.TestCase): headers = dict((k.lower(), v) for k, v in resp.getheaders()) self.assertEqual(headers.get('access-control-allow-origin'), '*') + self.assertIn('x-object-meta-color', headers.get( + 'access-control-expose-headers').split(', ')) #################### diff --git a/test/unit/common/middleware/crypto/test_decrypter.py b/test/unit/common/middleware/crypto/test_decrypter.py index e6d83b78fe..16ebf2823d 100644 --- a/test/unit/common/middleware/crypto/test_decrypter.py +++ b/test/unit/common/middleware/crypto/test_decrypter.py @@ -125,6 +125,7 @@ class TestDecrypterObjectRequests(unittest.TestCase): resp.headers['X-Object-Sysmeta-Container-Update-Override-Etag']) self.assertNotIn('X-Object-Sysmeta-Crypto-Body-Meta', resp.headers) self.assertNotIn('X-Object-Sysmeta-Crypto-Etag', resp.headers) + self.assertNotIn('Access-Control-Expose-Headers', resp.headers) return resp def test_GET_success(self): @@ -226,6 +227,7 @@ class TestDecrypterObjectRequests(unittest.TestCase): self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual('text/plain', resp.headers['Content-Type']) self.assertEqual('encrypt me', resp.headers['x-object-meta-test']) + self.assertNotIn('Access-Control-Expose-Headers', resp.headers) return resp def test_GET_unencrypted_data_and_encrypted_metadata(self): @@ -259,6 +261,7 @@ class TestDecrypterObjectRequests(unittest.TestCase): self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual('text/plain', resp.headers['Content-Type']) self.assertEqual('unencrypted', resp.headers['x-object-meta-test']) + self.assertNotIn('Access-Control-Expose-Headers', resp.headers) return resp def test_GET_encrypted_data_and_unencrypted_metadata(self): @@ -271,7 +274,8 @@ class TestDecrypterObjectRequests(unittest.TestCase): def test_headers_case(self): body = b'fAkE ApP' - req = Request.blank('/v1/a/c/o', body='FaKe') + req = Request.blank('/v1/a/c/o', body='FaKe', headers={ + 'Origin': 'http://example.com'}) req.environ[CRYPTO_KEY_CALLBACK] = fetch_crypto_keys plaintext_etag = md5hex(body) body_key = os.urandom(32) @@ -281,7 +285,10 @@ class TestDecrypterObjectRequests(unittest.TestCase): hdrs.update({ 'x-Object-mEta-ignoRes-caSe': 'thIs pArt WilL bE cOol', + 'access-control-Expose-Headers': 'x-object-meta-ignores-case', + 'access-control-allow-origin': '*', }) + self.assertNotIn('x-object-meta-test', [k.lower() for k in hdrs]) self.app.register( 'GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) @@ -296,6 +303,11 @@ class TestDecrypterObjectRequests(unittest.TestCase): 'X-Object-Meta-Ignores-Case': 'thIs pArt WilL bE cOol', 'X-Object-Sysmeta-Test': 'do not encrypt me', 'Content-Type': 'text/plain', + 'Access-Control-Expose-Headers': ', '.join([ + 'x-object-meta-ignores-case', + 'x-object-meta-test', + ]), + 'Access-Control-Allow-Origin': '*', } self.assertEqual(dict(headers), expected) self.assertEqual(b'fAkE ApP', b''.join(app_iter))