From 94a31f440d07284bdddde427887a1f97e3390746 Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Thu, 16 Jul 2026 20:48:30 +0000 Subject: [PATCH] fix: update _SERVICE_ACCOUNT_EMAIL_PATTERN to require .gserviceaccount.com suffix --- .../google/auth/compute_engine/_metadata.py | 4 +- .../tests/compute_engine/test__metadata.py | 6 +- .../tests/compute_engine/test_credentials.py | 168 ++++++++++++------ 3 files changed, 125 insertions(+), 53 deletions(-) diff --git a/packages/google-auth/google/auth/compute_engine/_metadata.py b/packages/google-auth/google/auth/compute_engine/_metadata.py index f8e1769334d2..1ea7792c2cdd 100644 --- a/packages/google-auth/google/auth/compute_engine/_metadata.py +++ b/packages/google-auth/google/auth/compute_engine/_metadata.py @@ -38,7 +38,9 @@ _LOGGER = logging.getLogger(__name__) -_SERVICE_ACCOUNT_EMAIL_PATTERN = re.compile(r"^[^@]+@[^@]+\.[^@]+$") +_SERVICE_ACCOUNT_EMAIL_PATTERN = re.compile( + r"^[^@]+@[^@]+\.gserviceaccount\.com$", re.IGNORECASE +) _GCE_DEFAULT_MDS_IP = "169.254.169.254" _GCE_DEFAULT_HOST = "metadata.google.internal" diff --git a/packages/google-auth/tests/compute_engine/test__metadata.py b/packages/google-auth/tests/compute_engine/test__metadata.py index 199683f7b8c2..0fae4bd6ef16 100644 --- a/packages/google-auth/tests/compute_engine/test__metadata.py +++ b/packages/google-auth/tests/compute_engine/test__metadata.py @@ -993,9 +993,13 @@ def test__is_service_account_email(): _metadata._is_service_account_email("my-sa@my-project.iam.gserviceaccount.com") is True ) - assert _metadata._is_service_account_email("test@example.com") is True + assert ( + _metadata._is_service_account_email("my-sa@my-project.iam.GSERVICEACCOUNT.COM") + is True + ) # Empty inputs and standard string placeholders + assert _metadata._is_service_account_email("test@example.com") is False assert _metadata._is_service_account_email("default") is False assert _metadata._is_service_account_email("") is False assert _metadata._is_service_account_email(None) is False diff --git a/packages/google-auth/tests/compute_engine/test_credentials.py b/packages/google-auth/tests/compute_engine/test_credentials.py index 8f8a17e94640..ab171c5a6041 100644 --- a/packages/google-auth/tests/compute_engine/test_credentials.py +++ b/packages/google-auth/tests/compute_engine/test_credentials.py @@ -46,7 +46,7 @@ ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = "gl-python/ auth/ auth-request-type/at cred-type/mds" ID_TOKEN_REQUEST_METRICS_HEADER_VALUE = "gl-python/ auth/ auth-request-type/it cred-type/mds" -FAKE_SERVICE_ACCOUNT_EMAIL = "foo@bar.com" +FAKE_SERVICE_ACCOUNT_EMAIL = "foo@project.iam.gserviceaccount.com" FAKE_QUOTA_PROJECT_ID = "fake-quota-project" FAKE_SCOPES = ["scope1", "scope2"] FAKE_DEFAULT_SCOPES = ["scope3", "scope4"] @@ -98,7 +98,7 @@ def test_refresh_success(self, get, utcnow): get.side_effect = [ { # First request is for sevice account info. - "email": "service-account@example.com", + "email": "service-account@project.iam.gserviceaccount.com", "scopes": ["one", "two"], }, { @@ -116,7 +116,10 @@ def test_refresh_success(self, get, utcnow): assert self.credentials.expiry == (utcnow() + datetime.timedelta(seconds=500)) # Check the credential info - assert self.credentials.service_account_email == "service-account@example.com" + assert ( + self.credentials.service_account_email + == "service-account@project.iam.gserviceaccount.com" + ) assert self.credentials._scopes == ["one", "two"] # Check that the credentials are valid (have a token and are not @@ -136,7 +139,7 @@ def test_refresh_success_with_scopes(self, get, utcnow, mock_metrics_header_valu get.side_effect = [ { # First request is for sevice account info. - "email": "service-account@example.com", + "email": "service-account@project.iam.gserviceaccount.com", "scopes": ["one", "two"], }, { @@ -156,7 +159,10 @@ def test_refresh_success_with_scopes(self, get, utcnow, mock_metrics_header_valu assert self.credentials.expiry == (utcnow() + datetime.timedelta(seconds=500)) # Check the credential info - assert self.credentials.service_account_email == "service-account@example.com" + assert ( + self.credentials.service_account_email + == "service-account@project.iam.gserviceaccount.com" + ) assert self.credentials._scopes == scopes # Check that the credentials are valid (have a token and are not @@ -195,7 +201,7 @@ def test_before_request_refreshes(self, get): get.side_effect = [ { # First request is for sevice account info. - "email": "service-account@example.com", + "email": "service-account@project.iam.gserviceaccount.com", "scopes": "one two", }, { @@ -300,7 +306,7 @@ def test_build_regional_access_boundary_lookup_url_default_email( self, mock_get_service_account_info ): mock_get_service_account_info.return_value = { - "email": "resolved-email@example.com" + "email": "resolved-email@project.iam.gserviceaccount.com" } creds = self.credentials creds._universe_domain_cached = True @@ -308,15 +314,17 @@ def test_build_regional_access_boundary_lookup_url_default_email( url = creds._build_regional_access_boundary_lookup_url(request=mock_request) mock_get_service_account_info.assert_called_once_with(mock_request, "default") - expected_url_standard = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/resolved-email@example.com/allowedLocations" - expected_url_mtls = "https://iamcredentials.mtls.googleapis.com/v1/projects/-/serviceAccounts/resolved-email@example.com/allowedLocations" + expected_url_standard = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/resolved-email@project.iam.gserviceaccount.com/allowedLocations" + expected_url_mtls = "https://iamcredentials.mtls.googleapis.com/v1/projects/-/serviceAccounts/resolved-email@project.iam.gserviceaccount.com/allowedLocations" assert url in (expected_url_standard, expected_url_mtls) @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) def test_build_regional_access_boundary_lookup_url_http_client_request( self, mock_get ): - mock_get.return_value = {"email": "resolved-email@example.com"} + mock_get.return_value = { + "email": "resolved-email@project.iam.gserviceaccount.com" + } creds = self.credentials creds._universe_domain_cached = True @@ -326,8 +334,8 @@ def test_build_regional_access_boundary_lookup_url_http_client_request( url = creds._build_regional_access_boundary_lookup_url(request=req) - expected_url_standard = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/resolved-email@example.com/allowedLocations" - expected_url_mtls = "https://iamcredentials.mtls.googleapis.com/v1/projects/-/serviceAccounts/resolved-email@example.com/allowedLocations" + expected_url_standard = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/resolved-email@project.iam.gserviceaccount.com/allowedLocations" + expected_url_mtls = "https://iamcredentials.mtls.googleapis.com/v1/projects/-/serviceAccounts/resolved-email@project.iam.gserviceaccount.com/allowedLocations" assert url in (expected_url_standard, expected_url_mtls) @mock.patch( @@ -352,7 +360,7 @@ def test_build_regional_access_boundary_lookup_url_explicit_email_standard( url = creds._build_regional_access_boundary_lookup_url() mock_get_service_account_info.assert_not_called() - expected_url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/foo@bar.com/allowedLocations" + expected_url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/foo@project.iam.gserviceaccount.com/allowedLocations" assert url == expected_url @mock.patch( @@ -377,7 +385,7 @@ def test_build_regional_access_boundary_lookup_url_explicit_email_mtls( url = creds._build_regional_access_boundary_lookup_url() mock_get_service_account_info.assert_not_called() - expected_url = "https://iamcredentials.mtls.googleapis.com/v1/projects/-/serviceAccounts/foo@bar.com/allowedLocations" + expected_url = "https://iamcredentials.mtls.googleapis.com/v1/projects/-/serviceAccounts/foo@project.iam.gserviceaccount.com/allowedLocations" assert url == expected_url @mock.patch( @@ -491,7 +499,10 @@ def test_refresh_with_agent_identity( mock_get_path.return_value = str(cert_path) mock_metadata_get.side_effect = [ - {"email": "service-account@example.com", "scopes": ["one", "two"]}, + { + "email": "service-account@project.iam.gserviceaccount.com", + "scopes": ["one", "two"], + }, {"access_token": "token", "expires_in": 500}, ] @@ -526,7 +537,10 @@ def test_refresh_with_agent_identity_opt_out_or_not_agent( mock_get_path.return_value = str(cert_path) mock_metadata_get.side_effect = [ - {"email": "service-account@example.com", "scopes": ["one", "two"]}, + { + "email": "service-account@project.iam.gserviceaccount.com", + "scopes": ["one", "two"], + }, {"access_token": "token", "expires_in": 500}, ] @@ -553,7 +567,10 @@ class TestIDTokenCredentials(object): @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) def test_default_state(self, get): get.side_effect = [ - {"email": "service-account@example.com", "scope": ["one", "two"]} + { + "email": "service-account@project.iam.gserviceaccount.com", + "scope": ["one", "two"], + } ] request = mock.create_autospec(transport.Request, instance=True) @@ -565,10 +582,16 @@ def test_default_state(self, get): # Expiration hasn't been set yet assert not self.credentials.expired # Service account email hasn't been populated - assert self.credentials.service_account_email == "service-account@example.com" + assert ( + self.credentials.service_account_email + == "service-account@project.iam.gserviceaccount.com" + ) # Signer is initialized assert self.credentials.signer - assert self.credentials.signer_email == "service-account@example.com" + assert ( + self.credentials.signer_email + == "service-account@project.iam.gserviceaccount.com" + ) # No quota project assert not self.credentials._quota_project_id @@ -580,7 +603,10 @@ def test_default_state(self, get): @mock.patch("google.auth.iam.Signer.sign", autospec=True) def test_make_authorization_grant_assertion(self, sign, get, utcnow): get.side_effect = [ - {"email": "service-account@example.com", "scopes": ["one", "two"]} + { + "email": "service-account@project.iam.gserviceaccount.com", + "scopes": ["one", "two"], + } ] sign.side_effect = [b"signature"] @@ -601,7 +627,7 @@ def test_make_authorization_grant_assertion(self, sign, get, utcnow): "aud": "https://www.googleapis.com/oauth2/v4/token", "exp": 3600, "iat": 0, - "iss": "service-account@example.com", + "iss": "service-account@project.iam.gserviceaccount.com", "target_audience": "https://audience.com", } @@ -645,7 +671,10 @@ def test_with_service_account(self, sign, get, utcnow): @mock.patch("google.auth.iam.Signer.sign", autospec=True) def test_additional_claims(self, sign, get, utcnow): get.side_effect = [ - {"email": "service-account@example.com", "scopes": ["one", "two"]} + { + "email": "service-account@project.iam.gserviceaccount.com", + "scopes": ["one", "two"], + } ] sign.side_effect = [b"signature"] @@ -668,7 +697,7 @@ def test_additional_claims(self, sign, get, utcnow): "aud": "https://www.googleapis.com/oauth2/v4/token", "exp": 3600, "iat": 0, - "iss": "service-account@example.com", + "iss": "service-account@project.iam.gserviceaccount.com", "target_audience": "https://audience.com", "foo": "bar", } @@ -679,7 +708,7 @@ def test_token_uri(self): self.credentials = credentials.IDTokenCredentials( request=request, signer=mock.Mock(), - service_account_email="foo@example.com", + service_account_email="foo@project.iam.gserviceaccount.com", target_audience="https://audience.com", ) assert self.credentials._token_uri == credentials._DEFAULT_TOKEN_URI @@ -687,7 +716,7 @@ def test_token_uri(self): self.credentials = credentials.IDTokenCredentials( request=request, signer=mock.Mock(), - service_account_email="foo@example.com", + service_account_email="foo@project.iam.gserviceaccount.com", target_audience="https://audience.com", token_uri="https://example.com/token", ) @@ -701,7 +730,10 @@ def test_token_uri(self): @mock.patch("google.auth.iam.Signer.sign", autospec=True) def test_with_target_audience(self, sign, get, utcnow): get.side_effect = [ - {"email": "service-account@example.com", "scopes": ["one", "two"]} + { + "email": "service-account@project.iam.gserviceaccount.com", + "scopes": ["one", "two"], + } ] sign.side_effect = [b"signature"] @@ -723,7 +755,7 @@ def test_with_target_audience(self, sign, get, utcnow): "aud": "https://www.googleapis.com/oauth2/v4/token", "exp": 3600, "iat": 0, - "iss": "service-account@example.com", + "iss": "service-account@project.iam.gserviceaccount.com", "target_audience": "https://actually.not", } @@ -748,7 +780,7 @@ def test_with_target_audience_integration(self): content_type="application/json", json={ "scopes": "email", - "email": "service-account@example.com", + "email": "service-account@project.iam.gserviceaccount.com", "aliases": ["default"], }, ) @@ -792,7 +824,7 @@ def test_with_target_audience_integration(self): responses.POST, re.compile( r"https://iamcredentials\.(mtls\.)?googleapis\.com/v1/projects/-/" - r"serviceAccounts/service-account@example\.com:signBlob" + r"serviceAccounts/service-account@project\.iam\.gserviceaccount\.com:signBlob" ), status=200, content_type="application/json", @@ -816,7 +848,7 @@ def test_with_target_audience_integration(self): self.credentials = credentials.IDTokenCredentials( request=requests.Request(), - service_account_email="service-account@example.com", + service_account_email="service-account@project.iam.gserviceaccount.com", target_audience="https://audience.com", ) @@ -834,7 +866,10 @@ def test_with_target_audience_integration(self): @mock.patch("google.auth.iam.Signer.sign", autospec=True) def test_with_quota_project(self, sign, get, utcnow): get.side_effect = [ - {"email": "service-account@example.com", "scopes": ["one", "two"]} + { + "email": "service-account@project.iam.gserviceaccount.com", + "scopes": ["one", "two"], + } ] sign.side_effect = [b"signature"] @@ -858,7 +893,7 @@ def test_with_quota_project(self, sign, get, utcnow): "aud": "https://www.googleapis.com/oauth2/v4/token", "exp": 3600, "iat": 0, - "iss": "service-account@example.com", + "iss": "service-account@project.iam.gserviceaccount.com", "target_audience": "https://audience.com", } @@ -878,7 +913,10 @@ def test_with_quota_project(self, sign, get, utcnow): @mock.patch("google.auth.iam.Signer.sign", autospec=True) def test_with_token_uri(self, sign, get, utcnow): get.side_effect = [ - {"email": "service-account@example.com", "scopes": ["one", "two"]} + { + "email": "service-account@project.iam.gserviceaccount.com", + "scopes": ["one", "two"], + } ] sign.side_effect = [b"signature"] @@ -900,7 +938,10 @@ def test_with_token_uri(self, sign, get, utcnow): @mock.patch("google.auth.iam.Signer.sign", autospec=True) def test_with_token_uri_exception(self, sign, get, utcnow): get.side_effect = [ - {"email": "service-account@example.com", "scopes": ["one", "two"]} + { + "email": "service-account@project.iam.gserviceaccount.com", + "scopes": ["one", "two"], + } ] sign.side_effect = [b"signature"] @@ -932,7 +973,7 @@ def test_with_quota_project_integration(self): content_type="application/json", json={ "scopes": "email", - "email": "service-account@example.com", + "email": "service-account@project.iam.gserviceaccount.com", "aliases": ["default"], }, ) @@ -976,7 +1017,7 @@ def test_with_quota_project_integration(self): responses.POST, re.compile( r"https://iamcredentials\.(mtls\.)?googleapis\.com/v1/projects/-/" - r"serviceAccounts/service-account@example\.com:signBlob" + r"serviceAccounts/service-account@project\.iam\.gserviceaccount\.com:signBlob" ), status=200, content_type="application/json", @@ -1000,7 +1041,7 @@ def test_with_quota_project_integration(self): self.credentials = credentials.IDTokenCredentials( request=requests.Request(), - service_account_email="service-account@example.com", + service_account_email="service-account@project.iam.gserviceaccount.com", target_audience="https://audience.com", ) @@ -1020,7 +1061,10 @@ def test_with_quota_project_integration(self): @mock.patch("google.oauth2._client.id_token_jwt_grant", autospec=True) def test_refresh_success(self, id_token_jwt_grant, sign, get, utcnow): get.side_effect = [ - {"email": "service-account@example.com", "scopes": ["one", "two"]} + { + "email": "service-account@project.iam.gserviceaccount.com", + "scopes": ["one", "two"], + } ] sign.side_effect = [b"signature"] id_token_jwt_grant.side_effect = [ @@ -1044,7 +1088,10 @@ def test_refresh_success(self, id_token_jwt_grant, sign, get, utcnow): assert self.credentials.expiry == _helpers.utcfromtimestamp(3600) # Check the credential info - assert self.credentials.service_account_email == "service-account@example.com" + assert ( + self.credentials.service_account_email + == "service-account@project.iam.gserviceaccount.com" + ) # Check that the credentials are valid (have a token and are not # expired) @@ -1058,7 +1105,10 @@ def test_refresh_success(self, id_token_jwt_grant, sign, get, utcnow): @mock.patch("google.auth.iam.Signer.sign", autospec=True) def test_refresh_error(self, sign, get, utcnow): get.side_effect = [ - {"email": "service-account@example.com", "scopes": ["one", "two"]} + { + "email": "service-account@project.iam.gserviceaccount.com", + "scopes": ["one", "two"], + } ] sign.side_effect = [b"signature"] @@ -1086,7 +1136,10 @@ def test_refresh_error(self, sign, get, utcnow): @mock.patch("google.oauth2._client.id_token_jwt_grant", autospec=True) def test_before_request_refreshes(self, id_token_jwt_grant, sign, get, utcnow): get.side_effect = [ - {"email": "service-account@example.com", "scopes": "one two"} + { + "email": "service-account@project.iam.gserviceaccount.com", + "scopes": "one two", + } ] sign.side_effect = [b"signature"] id_token_jwt_grant.side_effect = [ @@ -1119,7 +1172,10 @@ def test_before_request_refreshes(self, id_token_jwt_grant, sign, get, utcnow): @mock.patch("google.auth.iam.Signer.sign", autospec=True) def test_sign_bytes(self, sign, get): get.side_effect = [ - {"email": "service-account@example.com", "scopes": ["one", "two"]} + { + "email": "service-account@project.iam.gserviceaccount.com", + "scopes": ["one", "two"], + } ] sign.side_effect = [b"signature"] @@ -1151,7 +1207,9 @@ def test_get_id_token_from_metadata( self, get, get_service_account_info, mock_metrics_header_value ): get.return_value = SAMPLE_ID_TOKEN - get_service_account_info.return_value = {"email": "foo@example.com"} + get_service_account_info.return_value = { + "email": "foo@project.iam.gserviceaccount.com" + } cred = credentials.IDTokenCredentials( mock.Mock(), "audience", use_metadata_identity_endpoint=True @@ -1167,7 +1225,7 @@ def test_get_id_token_from_metadata( assert cred._use_metadata_identity_endpoint assert cred._signer is None assert cred._token_uri is None - assert cred._service_account_email == "foo@example.com" + assert cred._service_account_email == "foo@project.iam.gserviceaccount.com" assert cred._target_audience == "audience" with pytest.raises(ValueError): cred.sign_bytes(b"bytes") @@ -1176,7 +1234,9 @@ def test_get_id_token_from_metadata( "google.auth.compute_engine._metadata.get_service_account_info", autospec=True ) def test_with_target_audience_for_metadata(self, get_service_account_info): - get_service_account_info.return_value = {"email": "foo@example.com"} + get_service_account_info.return_value = { + "email": "foo@project.iam.gserviceaccount.com" + } cred = credentials.IDTokenCredentials( mock.Mock(), "audience", use_metadata_identity_endpoint=True @@ -1187,13 +1247,15 @@ def test_with_target_audience_for_metadata(self, get_service_account_info): assert cred._use_metadata_identity_endpoint assert cred._signer is None assert cred._token_uri is None - assert cred._service_account_email == "foo@example.com" + assert cred._service_account_email == "foo@project.iam.gserviceaccount.com" @mock.patch( "google.auth.compute_engine._metadata.get_service_account_info", autospec=True ) def test_id_token_with_quota_project(self, get_service_account_info): - get_service_account_info.return_value = {"email": "foo@example.com"} + get_service_account_info.return_value = { + "email": "foo@project.iam.gserviceaccount.com" + } cred = credentials.IDTokenCredentials( mock.Mock(), "audience", use_metadata_identity_endpoint=True @@ -1204,7 +1266,7 @@ def test_id_token_with_quota_project(self, get_service_account_info): assert cred._use_metadata_identity_endpoint assert cred._signer is None assert cred._token_uri is None - assert cred._service_account_email == "foo@example.com" + assert cred._service_account_email == "foo@project.iam.gserviceaccount.com" @mock.patch( "google.auth.compute_engine._metadata.get_service_account_info", autospec=True @@ -1212,7 +1274,9 @@ def test_id_token_with_quota_project(self, get_service_account_info): @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) def test_invalid_id_token_from_metadata(self, get, get_service_account_info): get.return_value = "invalid_id_token" - get_service_account_info.return_value = {"email": "foo@example.com"} + get_service_account_info.return_value = { + "email": "foo@project.iam.gserviceaccount.com" + } cred = credentials.IDTokenCredentials( mock.Mock(), "audience", use_metadata_identity_endpoint=True @@ -1227,7 +1291,9 @@ def test_invalid_id_token_from_metadata(self, get, get_service_account_info): @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) def test_transport_error_from_metadata(self, get, get_service_account_info): get.side_effect = exceptions.TransportError("transport error") - get_service_account_info.return_value = {"email": "foo@example.com"} + get_service_account_info.return_value = { + "email": "foo@project.iam.gserviceaccount.com" + } cred = credentials.IDTokenCredentials( mock.Mock(), "audience", use_metadata_identity_endpoint=True @@ -1264,5 +1330,5 @@ def test_get_id_token_from_metadata_constructor(self): mock.Mock(), "audience", use_metadata_identity_endpoint=True, - service_account_email="foo@example.com", + service_account_email="foo@project.iam.gserviceaccount.com", )