diff --git a/CHANGELOG.md b/CHANGELOG.md index b9c164ba4..d2b69ade6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ [1]: https://pypi.org/project/google-auth/#history +## [2.23.1](https://github.com/googleapis/google-auth-library-python/compare/v2.23.0...v2.23.1) (2023-09-26) + + +### Bug Fixes + +* Less restrictive content-type header check for google authentication (ignores charset) ([#1382](https://github.com/googleapis/google-auth-library-python/issues/1382)) ([7039beb](https://github.com/googleapis/google-auth-library-python/commit/7039beb63b8644be748cfc2fc79a2b8b643cda9f)) +* Trust boundary meta header renaming and using the schema from backend team. ([#1384](https://github.com/googleapis/google-auth-library-python/issues/1384)) ([2503d4a](https://github.com/googleapis/google-auth-library-python/commit/2503d4a50995d4f2756846a17b33997273ace5f1)) +* Update urllib3 to >= 2.0.5 ([#1389](https://github.com/googleapis/google-auth-library-python/issues/1389)) ([a99f3bb](https://github.com/googleapis/google-auth-library-python/commit/a99f3bbf97c07a87203b7471817cfb2a1662293d)) + ## [2.23.0](https://github.com/googleapis/google-auth-library-python/compare/v2.22.0...v2.23.0) (2023-09-11) diff --git a/google/auth/_helpers.py b/google/auth/_helpers.py index ad2c095f2..f321bc834 100644 --- a/google/auth/_helpers.py +++ b/google/auth/_helpers.py @@ -17,6 +17,7 @@ import base64 import calendar import datetime +from email.message import Message import sys import urllib @@ -63,6 +64,28 @@ def decorator(method): return decorator +def parse_content_type(header_value): + """Parse a 'content-type' header value to get just the plain media-type (without parameters). + + This is done using the class Message from email.message as suggested in PEP 594 + (because the cgi is now deprecated and will be removed in python 3.13, + see https://peps.python.org/pep-0594/#cgi). + + Args: + header_value (str): The value of a 'content-type' header as a string. + + Returns: + str: A string with just the lowercase media-type from the parsed 'content-type' header. + If the provided content-type is not parsable, returns 'text/plain', + the default value for textual files. + """ + m = Message() + m["content-type"] = header_value + return ( + m.get_content_type() + ) # Despite the name, actually returns just the media-type + + def utcnow(): """Returns the current UTC datetime. diff --git a/google/auth/compute_engine/_metadata.py b/google/auth/compute_engine/_metadata.py index 04abe178f..1b2f5161a 100644 --- a/google/auth/compute_engine/_metadata.py +++ b/google/auth/compute_engine/_metadata.py @@ -218,7 +218,10 @@ def get( if response.status == http_client.OK: content = _helpers.from_bytes(response.data) - if response.headers["content-type"] == "application/json": + if ( + _helpers.parse_content_type(response.headers["content-type"]) + == "application/json" + ): try: return json.loads(content) except ValueError as caught_exc: diff --git a/google/auth/credentials.py b/google/auth/credentials.py index 80a2a5c0b..800781c40 100644 --- a/google/auth/credentials.py +++ b/google/auth/credentials.py @@ -52,8 +52,9 @@ def __init__(self): self._quota_project_id = None """Optional[str]: Project to use for quota and billing purposes.""" self._trust_boundary = None - """Optional[str]: Encoded string representation of credentials trust - boundary.""" + """Optional[dict]: Cache of a trust boundary response which has a list + of allowed regions and an encoded string representation of credentials + trust boundary.""" self._universe_domain = "googleapis.com" """Optional[str]: The universe domain value, default is googleapis.com """ @@ -135,8 +136,21 @@ def apply(self, headers, token=None): headers["authorization"] = "Bearer {}".format( _helpers.from_bytes(token or self.token) ) + """Trust boundary value will be a cached value from global lookup. + + The response of trust boundary will be a list of regions and a hex + encoded representation. + + An example of global lookup response: + { + "locations": [ + "us-central1", "us-east1", "europe-west1", "asia-east1" + ] + "encoded_locations": "0xA30" + } + """ if self._trust_boundary is not None: - headers["x-identity-trust-boundary"] = self._trust_boundary + headers["x-allowed-locations"] = self._trust_boundary["encoded_locations"] if self.quota_project_id: headers["x-goog-user-project"] = self.quota_project_id diff --git a/google/auth/external_account.py b/google/auth/external_account.py index c45e6f213..28b004c5f 100644 --- a/google/auth/external_account.py +++ b/google/auth/external_account.py @@ -132,7 +132,10 @@ def __init__( self._default_scopes = default_scopes self._workforce_pool_user_project = workforce_pool_user_project self._universe_domain = universe_domain or _DEFAULT_UNIVERSE_DOMAIN - self._trust_boundary = "0" # expose a placeholder trust boundary value. + self._trust_boundary = { + "locations": [], + "encoded_locations": "0x0", + } # expose a placeholder trust boundary value. if self._client_id: self._client_auth = utils.ClientAuthentication( diff --git a/google/auth/transport/urllib3.py b/google/auth/transport/urllib3.py index 053d6f7b7..bc4de4d14 100644 --- a/google/auth/transport/urllib3.py +++ b/google/auth/transport/urllib3.py @@ -179,7 +179,7 @@ def _make_mutual_tls_http(cert, key): return http -class AuthorizedHttp(urllib3.request.RequestMethods): +class AuthorizedHttp(urllib3._request_methods.RequestMethods): # type: ignore """A urllib3 HTTP class with credentials. This class is used to perform requests to API endpoints that require diff --git a/google/auth/version.py b/google/auth/version.py index 491187e6d..19ff1bf7d 100644 --- a/google/auth/version.py +++ b/google/auth/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.23.0" +__version__ = "2.23.1" diff --git a/google/oauth2/service_account.py b/google/oauth2/service_account.py index e08899f8e..803b13070 100644 --- a/google/oauth2/service_account.py +++ b/google/oauth2/service_account.py @@ -196,7 +196,7 @@ def __init__( self._additional_claims = additional_claims else: self._additional_claims = {} - self._trust_boundary = "0" + self._trust_boundary = {"locations": [], "encoded_locations": "0x0"} @classmethod def _from_signer_and_info(cls, signer, info, **kwargs): diff --git a/setup.py b/setup.py index 922c505e6..4ec9b19c6 100644 --- a/setup.py +++ b/setup.py @@ -25,8 +25,7 @@ # rsa==4.5 is the last version to support 2.7 # https://github.com/sybrenstuvel/python-rsa/issues/152#issuecomment-643470233 "rsa>=3.1.4,<5", - # install enum34 to support 2.7. enum34 only works up to python version 3.3. - "urllib3<2.0", + "urllib3>=2.0.5", ) extras = { diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index 1409f0b83..a53743734 100644 Binary files a/system_tests/secrets.tar.enc and b/system_tests/secrets.tar.enc differ diff --git a/tests/compute_engine/test__metadata.py b/tests/compute_engine/test__metadata.py index a940feb25..f0e432979 100644 --- a/tests/compute_engine/test__metadata.py +++ b/tests/compute_engine/test__metadata.py @@ -176,6 +176,24 @@ def test_get_success_json(): assert result[key] == value +def test_get_success_json_content_type_charset(): + key, value = "foo", "bar" + + data = json.dumps({key: value}) + request = make_request( + data, headers={"content-type": "application/json; charset=UTF-8"} + ) + + result = _metadata.get(request, PATH) + + request.assert_called_once_with( + method="GET", + url=_metadata._METADATA_ROOT + PATH, + headers=_metadata._METADATA_HEADERS, + ) + assert result[key] == value + + def test_get_success_retry(): key, value = "foo", "bar" diff --git a/tests/test__helpers.py b/tests/test__helpers.py index c1f1d812e..c9a3847ac 100644 --- a/tests/test__helpers.py +++ b/tests/test__helpers.py @@ -51,6 +51,32 @@ def func2(): # pragma: NO COVER _helpers.copy_docstring(SourceClass)(func2) +def test_parse_content_type_plain(): + assert _helpers.parse_content_type("text/html") == "text/html" + assert _helpers.parse_content_type("application/xml") == "application/xml" + assert _helpers.parse_content_type("application/json") == "application/json" + + +def test_parse_content_type_with_parameters(): + content_type_html = "text/html; charset=UTF-8" + content_type_xml = "application/xml; charset=UTF-16; version=1.0" + content_type_json = "application/json; charset=UTF-8; indent=2" + assert _helpers.parse_content_type(content_type_html) == "text/html" + assert _helpers.parse_content_type(content_type_xml) == "application/xml" + assert _helpers.parse_content_type(content_type_json) == "application/json" + + +def test_parse_content_type_missing_or_broken(): + content_type_foo = None + content_type_bar = "" + content_type_baz = "1234" + content_type_qux = " ; charset=UTF-8" + assert _helpers.parse_content_type(content_type_foo) == "text/plain" + assert _helpers.parse_content_type(content_type_bar) == "text/plain" + assert _helpers.parse_content_type(content_type_baz) == "text/plain" + assert _helpers.parse_content_type(content_type_qux) == "text/plain" + + def test_utcnow(): assert isinstance(_helpers.utcnow(), datetime.datetime) diff --git a/tests/test_aws.py b/tests/test_aws.py index 39138ab12..db2e98410 100644 --- a/tests/test_aws.py +++ b/tests/test_aws.py @@ -1969,7 +1969,7 @@ def test_refresh_success_with_impersonation_ignore_default_scopes( "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), "x-goog-user-project": QUOTA_PROJECT_ID, "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, @@ -2066,7 +2066,7 @@ def test_refresh_success_with_impersonation_use_default_scopes( "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), "x-goog-user-project": QUOTA_PROJECT_ID, "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, diff --git a/tests/test_credentials.py b/tests/test_credentials.py index 99235cda6..5eee35c98 100644 --- a/tests/test_credentials.py +++ b/tests/test_credentials.py @@ -81,7 +81,7 @@ def test_before_request(): assert credentials.valid assert credentials.token == "token" assert headers["authorization"] == "Bearer token" - assert "x-identity-trust-boundary" not in headers + assert "x-allowed-locations" not in headers request = "token2" headers = {} @@ -91,13 +91,13 @@ def test_before_request(): assert credentials.valid assert credentials.token == "token" assert headers["authorization"] == "Bearer token" - assert "x-identity-trust-boundary" not in headers + assert "x-allowed-locations" not in headers def test_before_request_with_trust_boundary(): - DUMMY_BOUNDARY = "00110101" + DUMMY_BOUNDARY = "0xA30" credentials = CredentialsImpl() - credentials._trust_boundary = DUMMY_BOUNDARY + credentials._trust_boundary = {"locations": [], "encoded_locations": DUMMY_BOUNDARY} request = "token" headers = {} @@ -106,7 +106,7 @@ def test_before_request_with_trust_boundary(): assert credentials.valid assert credentials.token == "token" assert headers["authorization"] == "Bearer token" - assert headers["x-identity-trust-boundary"] == DUMMY_BOUNDARY + assert headers["x-allowed-locations"] == DUMMY_BOUNDARY request = "token2" headers = {} @@ -116,7 +116,7 @@ def test_before_request_with_trust_boundary(): assert credentials.valid assert credentials.token == "token" assert headers["authorization"] == "Bearer token" - assert headers["x-identity-trust-boundary"] == DUMMY_BOUNDARY + assert headers["x-allowed-locations"] == DUMMY_BOUNDARY def test_before_request_metrics(): diff --git a/tests/test_external_account.py b/tests/test_external_account.py index 0b165bc70..6f6e18b2c 100644 --- a/tests/test_external_account.py +++ b/tests/test_external_account.py @@ -833,7 +833,7 @@ def test_refresh_impersonation_without_client_auth_success( "Content-Type": "application/json", "authorization": "Bearer {}".format(token_response["access_token"]), "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, @@ -915,7 +915,7 @@ def test_refresh_workforce_impersonation_without_client_auth_success( "Content-Type": "application/json", "authorization": "Bearer {}".format(token_response["access_token"]), "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, @@ -1134,7 +1134,7 @@ def test_refresh_impersonation_with_client_auth_success_ignore_default_scopes( "Content-Type": "application/json", "authorization": "Bearer {}".format(token_response["access_token"]), "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, @@ -1218,7 +1218,7 @@ def test_refresh_impersonation_with_client_auth_success_use_default_scopes( "Content-Type": "application/json", "authorization": "Bearer {}".format(token_response["access_token"]), "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, @@ -1274,7 +1274,7 @@ def test_apply_without_quota_project_id(self): assert headers == { "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } def test_apply_workforce_without_quota_project_id(self): @@ -1291,7 +1291,7 @@ def test_apply_workforce_without_quota_project_id(self): assert headers == { "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } def test_apply_impersonation_without_quota_project_id(self): @@ -1323,7 +1323,7 @@ def test_apply_impersonation_without_quota_project_id(self): assert headers == { "authorization": "Bearer {}".format(impersonation_response["accessToken"]), - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } def test_apply_with_quota_project_id(self): @@ -1340,7 +1340,7 @@ def test_apply_with_quota_project_id(self): "other": "header-value", "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), "x-goog-user-project": self.QUOTA_PROJECT_ID, - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } def test_apply_impersonation_with_quota_project_id(self): @@ -1375,7 +1375,7 @@ def test_apply_impersonation_with_quota_project_id(self): "other": "header-value", "authorization": "Bearer {}".format(impersonation_response["accessToken"]), "x-goog-user-project": self.QUOTA_PROJECT_ID, - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } def test_before_request(self): @@ -1391,7 +1391,7 @@ def test_before_request(self): assert headers == { "other": "header-value", "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } # Second call shouldn't call refresh. @@ -1400,7 +1400,7 @@ def test_before_request(self): assert headers == { "other": "header-value", "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } def test_before_request_workforce(self): @@ -1418,7 +1418,7 @@ def test_before_request_workforce(self): assert headers == { "other": "header-value", "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } # Second call shouldn't call refresh. @@ -1427,7 +1427,7 @@ def test_before_request_workforce(self): assert headers == { "other": "header-value", "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } def test_before_request_impersonation(self): @@ -1458,7 +1458,7 @@ def test_before_request_impersonation(self): assert headers == { "other": "header-value", "authorization": "Bearer {}".format(impersonation_response["accessToken"]), - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } # Second call shouldn't call refresh. @@ -1467,7 +1467,7 @@ def test_before_request_impersonation(self): assert headers == { "other": "header-value", "authorization": "Bearer {}".format(impersonation_response["accessToken"]), - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } @mock.patch("google.auth._helpers.utcnow") @@ -1495,7 +1495,7 @@ def test_before_request_expired(self, utcnow): # Cached token should be used. assert headers == { "authorization": "Bearer token", - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } # Next call should simulate 1 second passed. @@ -1509,7 +1509,7 @@ def test_before_request_expired(self, utcnow): # New token should be retrieved. assert headers == { "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } @mock.patch("google.auth._helpers.utcnow") @@ -1552,7 +1552,7 @@ def test_before_request_impersonation_expired(self, utcnow): # Cached token should be used. assert headers == { "authorization": "Bearer token", - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } # Next call should simulate 1 second passed. This will trigger the expiration @@ -1567,7 +1567,7 @@ def test_before_request_impersonation_expired(self, utcnow): # New token should be retrieved. assert headers == { "authorization": "Bearer {}".format(impersonation_response["accessToken"]), - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } @pytest.mark.parametrize( @@ -1666,7 +1666,7 @@ def test_get_project_id_cloud_resource_manager_success( "x-goog-user-project": self.QUOTA_PROJECT_ID, "authorization": "Bearer {}".format(token_response["access_token"]), "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, @@ -1720,7 +1720,7 @@ def test_get_project_id_cloud_resource_manager_success( "authorization": "Bearer {}".format( impersonation_response["accessToken"] ), - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", }, ) @@ -1792,7 +1792,7 @@ def test_workforce_pool_get_project_id_cloud_resource_manager_success( "authorization": "Bearer {}".format( self.SUCCESS_RESPONSE["access_token"] ), - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", }, ) @@ -1842,7 +1842,7 @@ def test_refresh_impersonation_with_lifetime( "Content-Type": "application/json", "authorization": "Bearer {}".format(token_response["access_token"]), "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, diff --git a/tests/test_identity_pool.py b/tests/test_identity_pool.py index e469cf731..8821df088 100644 --- a/tests/test_identity_pool.py +++ b/tests/test_identity_pool.py @@ -319,7 +319,7 @@ def assert_underlying_credentials_refresh( "Content-Type": "application/json", "authorization": "Bearer {}".format(token_response["access_token"]), "x-goog-api-client": metrics_header_value, - "x-identity-trust-boundary": "0", + "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None,