Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: rewrite object in CMEK enabled bucket #807

Merged
merged 1 commit into from
Jun 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 9 additions & 1 deletion google/cloud/storage/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -3576,7 +3576,15 @@ def rewrite(
if source.generation:
query_params["sourceGeneration"] = source.generation

if self.kms_key_name is not None:
# When a Customer Managed Encryption Key is used to encrypt Cloud Storage object
# at rest, object resource metadata will store the version of the Key Management
# Service cryptographic material. If a Blob instance with KMS Key metadata set is
# used to rewrite the object, then the existing kmsKeyName version
# value can't be used in the rewrite request and the client instead ignores it.
if (
self.kms_key_name is not None
and "cryptoKeyVersions" not in self.kms_key_name
):
query_params["destinationKmsKeyName"] = self.kms_key_name

_add_generation_match_parameters(
Expand Down
11 changes: 11 additions & 0 deletions tests/system/test_kms_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,17 @@ def test_blob_rewrite_rotate_csek_to_cmek(

assert dest.download_as_bytes() == source_data

# Test existing kmsKeyName version is ignored in the rewrite request
dest = kms_bucket.get_blob(blob_name)
source = kms_bucket.get_blob(blob_name)
token, rewritten, total = dest.rewrite(source)

while token is not None:
token, rewritten, total = dest.rewrite(source, token=token)

assert rewritten == len(source_data)
assert dest.download_as_bytes() == source_data


def test_blob_upload_w_bucket_cmek_enabled(
kms_bucket,
Expand Down
52 changes: 52 additions & 0 deletions tests/unit/test_blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -4942,6 +4942,58 @@ def test_rewrite_same_name_w_old_key_new_kms_key(self):
_target_object=dest,
)

def test_rewrite_same_name_w_kms_key_w_version(self):
blob_name = "blob"
source_key = b"01234567890123456789012345678901" # 32 bytes
source_key_b64 = base64.b64encode(source_key).rstrip().decode("ascii")
source_key_hash = hashlib.sha256(source_key).digest()
source_key_hash_b64 = base64.b64encode(source_key_hash).rstrip().decode("ascii")
dest_kms_resource = (
"projects/test-project-123/"
"locations/us/"
"keyRings/test-ring/"
"cryptoKeys/test-key"
"cryptoKeyVersions/1"
)
bytes_rewritten = object_size = 42
api_response = {
"totalBytesRewritten": bytes_rewritten,
"objectSize": object_size,
"done": True,
"resource": {"etag": "DEADBEEF"},
}
client = mock.Mock(spec=["_post_resource"])
client._post_resource.return_value = api_response
bucket = _Bucket(client=client)
source = self._make_one(blob_name, bucket=bucket, encryption_key=source_key)
dest = self._make_one(blob_name, bucket=bucket, kms_key_name=dest_kms_resource)

token, rewritten, size = dest.rewrite(source)

self.assertIsNone(token)
self.assertEqual(rewritten, bytes_rewritten)
self.assertEqual(size, object_size)

expected_path = f"/b/name/o/{blob_name}/rewriteTo/b/name/o/{blob_name}"
expected_data = {"kmsKeyName": dest_kms_resource}
# The kmsKeyName version value can't be used in the rewrite request,
# so the client instead ignores it.
expected_query_params = {}
expected_headers = {
"X-Goog-Copy-Source-Encryption-Algorithm": "AES256",
"X-Goog-Copy-Source-Encryption-Key": source_key_b64,
"X-Goog-Copy-Source-Encryption-Key-Sha256": source_key_hash_b64,
}
client._post_resource.assert_called_once_with(
expected_path,
expected_data,
query_params=expected_query_params,
headers=expected_headers,
timeout=self._get_default_timeout(),
retry=DEFAULT_RETRY_IF_GENERATION_SPECIFIED,
_target_object=dest,
)

def test_update_storage_class_invalid(self):
blob_name = "blob-name"
bucket = _Bucket()
Expand Down