diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c4728422..3e1d818ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ [1]: https://pypi.org/project/google-cloud-storage/#history +### [1.36.1](https://www.github.com/googleapis/python-storage/compare/v1.36.0...v1.36.1) (2021-02-19) + + +### Bug Fixes + +* allow metadata keys to be cleared ([#383](https://www.github.com/googleapis/python-storage/issues/383)) ([79d27da](https://www.github.com/googleapis/python-storage/commit/79d27da9fe842e44a9091076ea0ef52c5ef5ff72)), closes [#381](https://www.github.com/googleapis/python-storage/issues/381) +* allow signed url version v4 without signed credentials ([#356](https://www.github.com/googleapis/python-storage/issues/356)) ([3e69bf9](https://www.github.com/googleapis/python-storage/commit/3e69bf92496616c5de28094dd42260b35c3bf982)) +* correctly encode bytes for V2 signature ([#382](https://www.github.com/googleapis/python-storage/issues/382)) ([f44212b](https://www.github.com/googleapis/python-storage/commit/f44212b7b91a67ca661898400fe632f9fb3ec8f6)) + ## [1.36.0](https://www.github.com/googleapis/python-storage/compare/v1.35.1...v1.36.0) (2021-02-10) diff --git a/google/cloud/storage/_signing.py b/google/cloud/storage/_signing.py index 1382ebc77..16c9397e5 100644 --- a/google/cloud/storage/_signing.py +++ b/google/cloud/storage/_signing.py @@ -77,7 +77,7 @@ def get_signed_query_params_v2(credentials, expiration, string_to_sign): signed payload. """ ensure_signed_credentials(credentials) - signature_bytes = credentials.sign_bytes(string_to_sign) + signature_bytes = credentials.sign_bytes(string_to_sign.encode("ascii")) signature = base64.b64encode(signature_bytes) service_account_name = credentials.signer_email return { @@ -457,9 +457,12 @@ def generate_signed_url_v4( google-cloud-python/issues/922 .. _reference: https://cloud.google.com/storage/docs/reference-headers + :type credentials: :class:`google.auth.credentials.Signing` :param credentials: Credentials object with an associated private key to - sign text. + sign text. That credentials must provide signer_email + only if service_account_email and access_token are not + passed. :type resource: str :param resource: A pointer to a specific resource @@ -533,7 +536,6 @@ def generate_signed_url_v4( :returns: A signed URL you can use to access the resource until expiration. """ - ensure_signed_credentials(credentials) expiration_seconds = get_expiration_seconds_v4(expiration) if _request_timestamp is None: @@ -542,7 +544,11 @@ def generate_signed_url_v4( request_timestamp = _request_timestamp datestamp = _request_timestamp[:8] - client_email = credentials.signer_email + client_email = service_account_email + if not access_token or not service_account_email: + ensure_signed_credentials(credentials) + client_email = credentials.signer_email + credential_scope = "{}/auto/storage/goog4_request".format(datestamp) credential = "{}/{}".format(client_email, credential_scope) diff --git a/google/cloud/storage/blob.py b/google/cloud/storage/blob.py index c175b367e..33b809d3c 100644 --- a/google/cloud/storage/blob.py +++ b/google/cloud/storage/blob.py @@ -3620,13 +3620,16 @@ def metadata(self): def metadata(self, value): """Update arbitrary/application specific metadata for the object. + Values are stored to GCS as strings. To delete a key, set its value to + None and call blob.patch(). + See https://cloud.google.com/storage/docs/json_api/v1/objects :type value: dict :param value: The blob metadata to set. """ if value is not None: - value = {k: str(v) for k, v in value.items()} + value = {k: str(v) if v is not None else None for k, v in value.items()} self._patch_property("metadata", value) @property diff --git a/google/cloud/storage/version.py b/google/cloud/storage/version.py index 393fb4dcc..9eabbb6a7 100644 --- a/google/cloud/storage/version.py +++ b/google/cloud/storage/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.36.0" +__version__ = "1.36.1" diff --git a/tests/system/test_system.py b/tests/system/test_system.py index 5973e6c73..9e7a86c2f 100644 --- a/tests/system/test_system.py +++ b/tests/system/test_system.py @@ -874,6 +874,19 @@ def test_write_metadata(self): blob.content_type = "image/png" self.assertEqual(blob.content_type, "image/png") + metadata = {"foo": "Foo", "bar": "Bar"} + blob.metadata = metadata + blob.patch() + blob.reload() + self.assertEqual(blob.metadata, metadata) + + # Ensure that metadata keys can be deleted by setting equal to None. + new_metadata = {"foo": "Foo", "bar": None} + blob.metadata = new_metadata + blob.patch() + blob.reload() + self.assertEqual(blob.metadata, {"foo": "Foo"}) + def test_direct_write_and_read_into_file(self): blob = self.bucket.blob("MyBuffer") file_contents = b"Hello World" diff --git a/tests/unit/test__signing.py b/tests/unit/test__signing.py index b69f550c1..3eac70cc1 100644 --- a/tests/unit/test__signing.py +++ b/tests/unit/test__signing.py @@ -255,7 +255,7 @@ def test_it(self): "Signature": base64.b64encode(sig_bytes), } self.assertEqual(result, expected) - credentials.sign_bytes.assert_called_once_with(string_to_sign) + credentials.sign_bytes.assert_called_once_with(string_to_sign.encode("ascii")) class Test_get_canonical_headers(unittest.TestCase): @@ -420,7 +420,7 @@ def _generate_helper( string_to_sign = "\n".join(elements) - credentials.sign_bytes.assert_called_once_with(string_to_sign) + credentials.sign_bytes.assert_called_once_with(string_to_sign.encode("ascii")) scheme, netloc, path, qs, frag = urllib_parse.urlsplit(url) expected_scheme, expected_netloc, _, _, _ = urllib_parse.urlsplit( @@ -653,7 +653,22 @@ def test_w_custom_query_parameters_w_string_value(self): def test_w_custom_query_parameters_w_none_value(self): self._generate_helper(query_parameters={"qux": None}) - def test_with_access_token(self): + def test_with_access_token_and_service_account_email(self): + resource = "/name/path" + credentials = _make_credentials() + email = mock.sentinel.service_account_email + with mock.patch( + "google.cloud.storage._signing._sign_message", return_value=b"DEADBEEF" + ): + self._call_fut( + credentials, + resource=resource, + expiration=datetime.timedelta(days=5), + service_account_email=email, + access_token="token", + ) + + def test_with_access_token_and_service_account_email_and_signer_email(self): resource = "/name/path" signer_email = "service@example.com" credentials = _make_credentials(signer_email=signer_email) @@ -668,6 +683,39 @@ def test_with_access_token(self): access_token="token", ) + def test_with_signer_email(self): + resource = "/name/path" + signer_email = "service@example.com" + credentials = _make_credentials(signer_email=signer_email) + credentials.sign_bytes.return_value = b"DEADBEEF" + self._call_fut( + credentials, resource=resource, expiration=datetime.timedelta(days=5), + ) + + def test_with_service_account_email_and_signer_email(self): + resource = "/name/path" + signer_email = "service@example.com" + credentials = _make_credentials(signer_email=signer_email) + credentials.sign_bytes.return_value = b"DEADBEEF" + self._call_fut( + credentials, + resource=resource, + expiration=datetime.timedelta(days=5), + service_account_email=signer_email, + ) + + def test_with_token_and_signer_email(self): + resource = "/name/path" + signer_email = "service@example.com" + credentials = _make_credentials(signer_email=signer_email) + credentials.sign_bytes.return_value = b"DEADBEEF" + self._call_fut( + credentials, + resource=resource, + expiration=datetime.timedelta(days=5), + access_token="token", + ) + class Test_sign_message(unittest.TestCase): @staticmethod