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
This commit is contained in:
Tim Burke 2020-03-09 13:45:58 -07:00 committed by Tim Burke
parent 3bf7cf60b9
commit cd693e519e
3 changed files with 43 additions and 7 deletions

View File

@ -197,7 +197,7 @@ class DecrypterObjContext(BaseDecrypterContext):
result.append((new_prefix + short_name, decrypted_value)) result.append((new_prefix + short_name, decrypted_value))
return result 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. 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 # that map to the same x-object-meta- header names i.e. decrypted
# headers win over unexpected, unencrypted headers. # headers win over unexpected, unencrypted headers.
if post_keys: 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_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 return mod_hdr_pairs
def multipart_response_iter(self, resp, boundary, body_key, crypto_meta): def multipart_response_iter(self, resp, boundary, body_key, crypto_meta):
@ -326,7 +342,9 @@ class DecrypterObjContext(BaseDecrypterContext):
self._response_exc_info) self._response_exc_info)
return app_resp 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 \ if put_crypto_meta and req.method == 'GET' and \
is_success(self._get_status_int()): is_success(self._get_status_int()):

View File

@ -1542,7 +1542,7 @@ class TestObject(unittest.TestCase):
def put_obj(url, token, parsed, conn, obj): def put_obj(url, token, parsed, conn, obj):
conn.request( conn.request(
'PUT', '%s/%s/%s' % (parsed.path, self.container, obj), '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) return check_response(conn)
def check_cors(url, token, parsed, 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()) headers = dict((k.lower(), v) for k, v in resp.getheaders())
self.assertEqual(headers.get('access-control-allow-origin'), 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, resp = retry(check_cors,
'GET', 'cat', {'Origin': 'http://m.com'}) '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()) headers = dict((k.lower(), v) for k, v in resp.getheaders())
self.assertEqual(headers.get('access-control-allow-origin'), 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, resp = retry(check_cors,
'GET', 'cat', {'Origin': 'http://m.com', '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()) headers = dict((k.lower(), v) for k, v in resp.getheaders())
self.assertEqual(headers.get('access-control-allow-origin'), self.assertEqual(headers.get('access-control-allow-origin'),
'*') '*')
self.assertIn('x-object-meta-color', headers.get(
'access-control-expose-headers').split(', '))
#################### ####################

View File

@ -125,6 +125,7 @@ class TestDecrypterObjectRequests(unittest.TestCase):
resp.headers['X-Object-Sysmeta-Container-Update-Override-Etag']) 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-Body-Meta', resp.headers)
self.assertNotIn('X-Object-Sysmeta-Crypto-Etag', resp.headers) self.assertNotIn('X-Object-Sysmeta-Crypto-Etag', resp.headers)
self.assertNotIn('Access-Control-Expose-Headers', resp.headers)
return resp return resp
def test_GET_success(self): def test_GET_success(self):
@ -226,6 +227,7 @@ class TestDecrypterObjectRequests(unittest.TestCase):
self.assertEqual(plaintext_etag, resp.headers['Etag']) self.assertEqual(plaintext_etag, resp.headers['Etag'])
self.assertEqual('text/plain', resp.headers['Content-Type']) self.assertEqual('text/plain', resp.headers['Content-Type'])
self.assertEqual('encrypt me', resp.headers['x-object-meta-test']) self.assertEqual('encrypt me', resp.headers['x-object-meta-test'])
self.assertNotIn('Access-Control-Expose-Headers', resp.headers)
return resp return resp
def test_GET_unencrypted_data_and_encrypted_metadata(self): 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(plaintext_etag, resp.headers['Etag'])
self.assertEqual('text/plain', resp.headers['Content-Type']) self.assertEqual('text/plain', resp.headers['Content-Type'])
self.assertEqual('unencrypted', resp.headers['x-object-meta-test']) self.assertEqual('unencrypted', resp.headers['x-object-meta-test'])
self.assertNotIn('Access-Control-Expose-Headers', resp.headers)
return resp return resp
def test_GET_encrypted_data_and_unencrypted_metadata(self): def test_GET_encrypted_data_and_unencrypted_metadata(self):
@ -271,7 +274,8 @@ class TestDecrypterObjectRequests(unittest.TestCase):
def test_headers_case(self): def test_headers_case(self):
body = b'fAkE ApP' 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 req.environ[CRYPTO_KEY_CALLBACK] = fetch_crypto_keys
plaintext_etag = md5hex(body) plaintext_etag = md5hex(body)
body_key = os.urandom(32) body_key = os.urandom(32)
@ -281,7 +285,10 @@ class TestDecrypterObjectRequests(unittest.TestCase):
hdrs.update({ hdrs.update({
'x-Object-mEta-ignoRes-caSe': 'thIs pArt WilL bE cOol', '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( self.app.register(
'GET', '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs) '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-Meta-Ignores-Case': 'thIs pArt WilL bE cOol',
'X-Object-Sysmeta-Test': 'do not encrypt me', 'X-Object-Sysmeta-Test': 'do not encrypt me',
'Content-Type': 'text/plain', '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(dict(headers), expected)
self.assertEqual(b'fAkE ApP', b''.join(app_iter)) self.assertEqual(b'fAkE ApP', b''.join(app_iter))